feat: rejected and expired recipient filters

Add a Rejected tab and an Expired pseudo-status tab to the documents
list, and an orthogonal hasExpiredRecipients boolean to the public v2
document and envelope find APIs.

- Add shared hasExpiredRecipient EXISTS predicate (expiresAt in the
  past, NOT_SIGNED, role != CC) to find-documents, get-stats and
  find-envelopes
- Surface REJECTED in the documents tabs (backend already wired)
- Add EXPIRED extended status: where-clause branches, stats count
  (excluded from ALL since it overlaps PENDING), status display
  (TimerOff/orange), tabs, and empty-state copy
- Expose hasExpiredRecipients on GET /document and /envelope using an
  enum-transform schema to avoid the coerce "false" -> true footgun
- Document that redistributing renews expired signing links
- Add e2e coverage for the expired filter on both endpoints
This commit is contained in:
ephraimduncan
2026-05-28 23:17:32 +00:00
parent 22ceff43e3
commit e98b37bb10
17 changed files with 338 additions and 9 deletions
@@ -10,7 +10,19 @@ export const findEnvelopesRoute = authenticatedProcedure
.query(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const { query, type, templateId, page, perPage, orderByDirection, orderByColumn, source, status, folderId } = input;
const {
query,
type,
templateId,
page,
perPage,
orderByDirection,
orderByColumn,
source,
status,
hasExpiredRecipients,
folderId,
} = input;
ctx.logger.info({
input: {
@@ -19,6 +31,7 @@ export const findEnvelopesRoute = authenticatedProcedure
templateId,
source,
status,
hasExpiredRecipients,
folderId,
page,
perPage,
@@ -33,6 +46,7 @@ export const findEnvelopesRoute = authenticatedProcedure
query,
source,
status,
hasExpiredRecipients,
page,
perPage,
folderId,
@@ -20,6 +20,11 @@ export const ZFindEnvelopesRequestSchema = ZFindSearchParamsSchema.extend({
templateId: z.number().describe('Filter envelopes by the template ID used to create it.').optional(),
source: z.nativeEnum(DocumentSource).describe('Filter envelopes by how it was created.').optional(),
status: z.nativeEnum(DocumentStatus).describe('Filter envelopes by the current status.').optional(),
hasExpiredRecipients: z
.enum(['true', 'false'])
.describe('Filter for envelopes that have at least one recipient whose signing link has expired.')
.transform((value) => value === 'true')
.optional(),
folderId: z.string().describe('Filter envelopes by folder ID.').optional(),
orderByColumn: z.enum(['createdAt']).optional(),
orderByDirection: z.enum(['asc', 'desc']).describe('Sort direction.').default('desc'),
@@ -10,7 +10,7 @@ export const redistributeEnvelopeMeta: TrpcRouteMeta = {
path: '/envelope/redistribute',
summary: 'Redistribute envelope',
description:
'Redistribute the envelope to the provided recipients who have not actioned the envelope. Will use the distribution method set in the envelope',
'Redistribute the envelope to the provided recipients who have not actioned the envelope. Will use the distribution method set in the envelope. This also refreshes the signing-link expiration for the targeted unsigned recipients, renewing any expired links.',
tags: ['Envelope'],
},
};