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
@@ -19,6 +19,7 @@ export const ZFindDocumentsInternalResponseSchema = ZFindResultResponse.extend({
[ExtendedDocumentStatus.PENDING]: z.number(),
[ExtendedDocumentStatus.COMPLETED]: z.number(),
[ExtendedDocumentStatus.REJECTED]: z.number(),
[ExtendedDocumentStatus.EXPIRED]: z.number(),
[ExtendedDocumentStatus.INBOX]: z.number(),
[ExtendedDocumentStatus.ALL]: z.number(),
}),
@@ -11,7 +11,18 @@ export const findDocumentsRoute = authenticatedProcedure
.query(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const { query, templateId, page, perPage, orderByDirection, orderByColumn, source, status, folderId } = input;
const {
query,
templateId,
page,
perPage,
orderByDirection,
orderByColumn,
source,
status,
hasExpiredRecipients,
folderId,
} = input;
const documents = await findDocuments({
userId: user.id,
@@ -20,6 +31,7 @@ export const findDocumentsRoute = authenticatedProcedure
query,
source,
status,
hasExpiredRecipients,
page,
perPage,
folderId,
@@ -19,6 +19,11 @@ export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({
templateId: z.number().describe('Filter documents by the template ID used to create it.').optional(),
source: z.nativeEnum(DocumentSource).describe('Filter documents by how it was created.').optional(),
status: z.nativeEnum(DocumentStatus).describe('Filter documents by the current status').optional(),
hasExpiredRecipients: z
.enum(['true', 'false'])
.describe('Filter for documents that have at least one recipient whose signing link has expired.')
.transform((value) => value === 'true')
.optional(),
folderId: z.string().describe('Filter documents by folder ID').optional(),
orderByColumn: z.enum(['createdAt']).optional(),
orderByDirection: z.enum(['asc', 'desc']).describe('').default('desc'),
@@ -9,7 +9,7 @@ export const redistributeDocumentMeta: TrpcRouteMeta = {
path: '/document/redistribute',
summary: 'Redistribute document',
description:
'Redistribute the document to the provided recipients who have not actioned the document. Will use the distribution method set in the document',
'Redistribute the document to the provided recipients who have not actioned the document. Will use the distribution method set in the document. This also refreshes the signing-link expiration for the targeted unsigned recipients, renewing any expired links.',
tags: ['Document'],
},
};