feat: Person 2 — recipient reminder and owner digest email handlers#14
Conversation
📝 WalkthroughWalkthroughTwo email job handlers have been fully implemented: the owner reminder digest handler queries pending envelopes and sends a consolidated summary email to the owner, while the recipient reminder handler sends individual reminder emails to specific recipients with signing status tracking and audit logging. Changes
Sequence Diagram(s)sequenceDiagram
actor JobQueue as Job Queue
participant OwnerHandler as Owner Digest Handler
participant DB as Database<br/>(Prisma)
participant Mailer as Email Service
participant AuditTask as Task: Create<br/>Reminder Logs
JobQueue->>OwnerHandler: run({userId, teamId, envelopeIds})
OwnerHandler->>DB: findMany(Envelopes by id,teamId,userId,PENDING)
DB-->>OwnerHandler: envelopes[]
alt No envelopes or disabled
OwnerHandler-->>JobQueue: return early
end
OwnerHandler->>OwnerHandler: Build pendingDocuments<br/>(count NOT_SIGNED,<br/>compute daysRemaining)
OwnerHandler->>OwnerHandler: Render SenderReminderDigestEmailTemplate
OwnerHandler->>Mailer: sendMail(HTML + plaintext)
Mailer-->>OwnerHandler: email sent
OwnerHandler->>AuditTask: runTask('create-reminder-logs')
AuditTask-->>OwnerHandler: logs created
OwnerHandler-->>JobQueue: complete
sequenceDiagram
actor JobQueue as Job Queue
participant RecipientHandler as Recipient Reminder Handler
participant DB as Database<br/>(Prisma)
participant EmailContext as Email Context<br/>Service
participant Mailer as Email Service
participant SendTask as Task: Send Email
participant LogTask as Task: Create Logs
JobQueue->>RecipientHandler: run({envelopeId, recipientId})
RecipientHandler->>DB: Load envelope + recipient
DB-->>RecipientHandler: envelope, recipient
alt Missing or not PENDING/NOT_SIGNED
RecipientHandler-->>JobQueue: return/throw
end
RecipientHandler->>EmailContext: getEmailContext()
EmailContext-->>RecipientHandler: branding, language, senderEmail
RecipientHandler->>RecipientHandler: Initialize i18n<br/>Construct signing URL<br/>Compute daysRemaining
RecipientHandler->>RecipientHandler: Create DocumentReminderEmailTemplate
RecipientHandler->>Mailer: renderEmailWithI18N(HTML + plaintext)
Mailer-->>RecipientHandler: rendered email
RecipientHandler->>SendTask: runTask('send-reminder-email',<br/>sendMail)
SendTask-->>RecipientHandler: sent
RecipientHandler->>LogTask: runTask('create-reminder-log')<br/>& runTask('create-audit-log')
LogTask-->>RecipientHandler: logged
RecipientHandler-->>JobQueue: complete
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 30
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.ts (1)
9-20:⚠️ Potential issue | 🟠 MajorBump job definition version for this breaking payload change.
Line 9 introduces a new required field (
userId), but Line 20 keeps version1.0.0. That can break in-flight or retried jobs serialized with the old payload shape.Suggested fix
export const SEND_OWNER_REMINDER_DIGEST_EMAIL_JOB_DEFINITION = { id: SEND_OWNER_REMINDER_DIGEST_EMAIL_JOB_DEFINITION_ID, name: 'Send Owner Reminder Digest Email', - version: '1.0.0', + version: '2.0.0', trigger: {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.ts` around lines 9 - 20, The job payload schema SEND_OWNER_REMINDER_DIGEST_EMAIL_JOB_DEFINITION_SCHEMA was changed to add a required userId field but the job definition object SEND_OWNER_REMINDER_DIGEST_EMAIL_JOB_DEFINITION still uses version "1.0.0"; update the version string (e.g., to "1.1.0" or a new semver per your project policy) in SEND_OWNER_REMINDER_DIGEST_EMAIL_JOB_DEFINITION so that consumers and retry systems can detect the breaking payload change and handle in-flight jobs correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.handler.ts`:
- Around line 119-123: The createMany currently uses envelopeIds from the
payload which can include envelopes that were filtered out earlier; change the
log creation to use the actual queried envelopes collection (the envelopes
variable returned by the query) so only documents included in the digest get
logs. Inside the io.runTask('create-reminder-logs', ...) block, replace
prisma.documentReminderLog.createMany's data mapping from envelopeIds.map(...)
to envelopes.map(e => ({ envelopeId: e.id })) (or equivalent) and handle the
case of an empty envelopes array before calling createMany to avoid unnecessary
DB calls or FK errors.
In
`@packages/lib/jobs/definitions/emails/send-recipient-reminder-email.handler.ts`:
- Around line 28-65: Wrap the read steps in the job handler inside io.runTask to
follow idempotency guidelines: call io.runTask with distinct keys (e.g.,
`load-envelope-${envelopeId}`, `load-recipient-${envelopeId}-${recipientId}`,
`email-context-${envelopeId}-${recipientId}`) and move the
prisma.envelope.findFirst, prisma.recipient.findFirst and getEmailContext
invocations into the respective runTask callbacks so each read is executed under
io.runTask; keep existing error checks (Envelope/Recipient not found) and return
logic unchanged inside those callbacks.
In `@packages/lib/jobs/definitions/internal/send-reminders-sweep.handler.ts`:
- Around line 13-34: The initial Prisma query (prisma.envelope.findMany) in
send-reminders-sweep.handler should be executed inside an idempotent checkpoint
by wrapping it with io.runTask using a unique task key; change the code so the
call to prisma.envelope.findMany (including its where/include/take) is moved
into a function passed to io.runTask('send-reminders-sweep:fetch-envelopes',
async () => { ... }) so the database read becomes a managed task and the handler
follows the job-step wrapping convention.
- Around line 93-121: The recipient and digest job triggers must be wrapped in
io.runTask to ensure idempotency; replace the direct calls to jobs.triggerJob
inside the dueRecipients map and the dueEnvelopesByOwner map with
io.runTask(...) that returns the jobs.triggerJob promise, using unique keys such
as `trigger-recipient-${r.id}-${r.envelopeId}` for recipients and
`trigger-digest-${teamId}-${userId}` (or
`trigger-digest-${teamId}-${userId}-${ownerKey}`) for owner digests so
Promise.allSettled operates on runTask results and prevents duplicate triggers
on retries.
In `@packages/lib/translations/de/web.po`:
- Around line 62-64: The German translation entries for new strings are empty
and must be filled; locate the msgid "\"{title}\" has been successfully hidden"
(and the other newly added msgid entries referenced in this PR) in
packages/lib/translations/de/web.po and provide appropriate German msgstr
translations for each empty msgstr (ensure proper interpolation for placeholders
like {title}); update the corresponding msgstrs for all other missing entries
noted in the comment so the German locale no longer falls back to English.
- Around line 21-24: The extracted source messages bake English plural suffixes
into the text (e.g. the msgid " · {0} day{1} remaining") from usages in
template-document-reminder.tsx, template-sender-reminder-digest.tsx, and
document-reminder.tsx; replace the ternary/placeholder pattern with Lingui/ICU
plural syntax (or Lingui.plural()) in each component so the extractor emits
proper plural forms (e.g. use a single plural expression keyed on daysRemaining
instead of separate {0}/{1} placeholders), then regenerate the catalog so web.po
contains ICU plural entries instead of English-only suffix placeholders. Ensure
you update the source calls that render the message to pass daysRemaining as the
plural variable and remove the separate suffix placeholder usage.
In `@packages/lib/translations/en/web.po`:
- Around line 448-450: The reminder job is querying recipients by signingStatus
only (send-reminders-sweep handler) so non-signer roles
(approver/viewer/assistant) with SigningStatus.NOT_SIGNED receive
signer-specific copy in template-document-reminder.tsx; fix by updating the
send-reminders-sweep query to add a role filter (e.g., recipientRole ==
RecipientRole.SIGNER) so only signers are enqueued, or alternatively make
template-document-reminder.tsx and related message keys role-aware (check
recipient.role and render "your signature"/"please sign"/"Sign Document" only
for RecipientRole.SIGNER and use appropriate copy for other roles). Ensure
references to SigningStatus, send-reminders-sweep handler, and
template-document-reminder.tsx are updated consistently (also apply same change
to keys around lines 8310–8316 and 9180–9182).
In `@packages/lib/translations/es/web.po`:
- Around line 62-64: The Spanish translations for the new hide-state messages
are missing—populate the msgstr entries for the msgid strings "\"{title}\" has
been successfully hidden", "Document hidden", and "Template hidden" in
packages/lib/translations/es/web.po (these correspond to the
envelope-delete-dialog.tsx hide flow and the other occurrences noted) by
providing accurate Spanish translations for each msgid so the UI shows Spanish
text instead of falling back to English.
- Around line 10911-10913: The Spanish translation for the new guidance string
used in apps/remix/app/components/dialogs/team-member-create-dialog.tsx is
missing (msgid shown with empty msgstr); add a proper Spanish translation for
that msgid into the msgstr in packages/lib/translations/es/web.po so Spanish
users see the localized help text. Make sure the msgstr contains a faithful
Spanish rendering of the full sentence, preserving punctuation and em dashes,
and save the updated entry.
- Around line 21-25: The Spanish translation file has empty msgstr for the
owner-digest strings (e.g., msgid " · {0} day{1} remaining" referenced from
packages/email/template-components/template-sender-reminder-digest.tsx), causing
English to show to Spanish users; update the corresponding msgstr entries in
packages/lib/translations/es/web.po (and the other listed msgid blocks at the
specified ranges) with correct Spanish translations that preserve the
placeholders ({0}, {1}) and pluralization logic so the owner reminder digest
emails and settings labels display in Spanish.
- Around line 10026-10028: The Spanish translations for the template-duplication
UI are missing; add appropriate Spanish msgstr entries in
packages/lib/translations/es/web.po for the msgid "Template Duplicated" and the
other related msgid strings used by
apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx (duplicate
toast, error copy, and success confirmation) so the Spanish locale shows
localized text instead of English.
- Around line 453-455: The Spanish translations for the reminder email strings
are missing causing fallback to English; open the Spanish PO file entries
corresponding to the reminder msgids (for example the msgid "{senderName} is
waiting for your signature." from
packages/email/template-components/template-document-reminder.tsx and the audit
text "Reminder sent to {0}" plus the reminder email subject/body entries in the
ranges around 8272-8277 and 8305-8321) and provide proper Spanish msgstr values
for each untranslated msgid (translate subject, body, and audit text),
preserving placeholders like {senderName} and {0} exactly and keeping
ICU/formatting intact so the reminder flow and logs use Spanish.
In `@packages/lib/translations/fr/web.po`:
- Around line 62-65: The French translation entries are empty and will fallback
to English; update the msgstr for the shown msgid "\"{title}\" has been
successfully hidden" in packages/lib/translations/fr/web.po to a proper French
string (e.g. « "{title}" a bien été masqué(e) » adjusted for context), and
likewise fill the corresponding empty msgstr entries at the other referenced
offsets (the other newly introduced UI/admin keys) so they provide proper French
translations; ensure you preserve the msgid placeholders like {title} exactly
and run the PO linter/formatting after edits.
- Around line 21-25: The French .po entries for the reminder/digest msgid " ·
{0} day{1} remaining" (used by template-sender-reminder-digest.tsx) are missing
translations; update each empty msgstr for that msgid (and the other identical
reminder/digest msgids noted) with an appropriate French translation that
respects the placeholders {0} and {1} and pluralization (for example " · {0}
jour{1} restant" with {1} set to "s" when plural), ensuring you replace the
empty msgstr values across all listed ranges so French users see the localized
copy.
In `@packages/lib/translations/it/web.po`:
- Around line 10911-10913: The Italian translation is missing for the new
team-member guidance message used in
apps/remix/app/components/dialogs/team-member-create-dialog.tsx (the msgid
starting "To add members to this team, they must first be invited to the
organisation..."); update the corresponding msgstr in
packages/lib/translations/it/web.po with a proper Italian translation that
conveys the same permission guidance so the UI doesn't fall back to English.
- Around line 62-65: Add Italian translations for the untranslated message IDs
in the PO file: locate the msgid entries such as "\"{title}\" has been
successfully hidden" and the corresponding duplicate/hide strings referenced by
the ranges (4000-4002, 10026-10044, 10804-10806, 13519-13521) in
packages/lib/translations/it/web.po and fill each msgstr with the proper Italian
text (e.g., "\"{title}\" è stato nascosto con successo" or the appropriate
gender/number variants), ensuring all newly added hide and duplicate messages
are translated and that placeholders like {title} are preserved exactly.
- Around line 297-300: The Italian translation file is missing translations for
the new reminder/digest strings (e.g. msgid "{0} recipients pending" referenced
from template-sender-reminder-digest.tsx); open
packages/lib/translations/it/web.po and supply accurate Italian msgstr values
for that msgid and the other empty entries noted (lines ranges 339-341, 453-455,
5665-5667, 7382-7384, 801-815, 8272-8295, 8315-8321), preserving any
placeholders like {0} exactly and matching punctuation/capitalization from the
English msgids so the reminder emails, audit logs, and settings copy render
correctly in Italian.
- Around line 21-24: The msgid " · {0} day{1} remaining" in
template-sender-reminder-digest.tsx hardcodes English plural suffixes and must
be replaced with a Lingui/ICU plural form (use a single numeric placeholder and
an ICU plural expression) so translators can provide correct Italian
pluralization; update the source message(s) where similar "day{1}"/"day{0}"
patterns occur (including the other occurrences noted) to use ICU plurals,
update the corresponding msgid(s) accordingly, then regenerate the translation
catalog so the .po entries reflect the new ICU-formatted msgid(s).
In `@packages/lib/translations/ja/web.po`:
- Around line 62-64: The Japanese translations are missing for newly added UI
strings (e.g., the toast msgid "\"{title}\" has been successfully hidden" from
apps/remix/app/components/dialogs/envelope-delete-dialog.tsx) and several other
msgids referenced in packages/lib/translations/ja/web.po; update msgstr entries
for that msgid and the other untranslated msgids (ranges noted: 4000-4002,
7382-7384, 10026-10028, 10042-10044, 10804-10806, 10911-10913, 13519-13521) with
proper Japanese UI copy matching tone and placeholders (preserve "{title}" and
other interpolation tokens), ensuring pluralization/formatting matches original
messages and saving the changes in packages/lib/translations/ja/web.po.
- Around line 21-25: The PO entries for the new reminder email strings (e.g.
msgid " · {0} day{1} remaining" from the reminder template component) are still
untranslated (msgstr ""), so update the corresponding msgstr values with proper
Japanese translations, preserving placeholders {0}/{1} and plural/spacing
semantics; also find and translate the other untranslated reminder-related msgid
entries called out in the review (the other reminder subjects/body/audit
strings) so the ja locale ships fully translated.
In `@packages/lib/translations/ko/web.po`:
- Around line 21-25: Add Korean translations for the new reminder email strings
in packages/lib/translations/ko/web.po: replace empty msgstr entries for the
msgid " · {0} day{1} remaining" (from template-sender-reminder-digest.tsx) and
the other newly added recipient-reminder and owner-digest msgid entries
referenced in the comment (lines indicated: 297-300, 339-341, 453-455, 801-815,
5665-5667, 8289-8321) with accurate Korean equivalents, ensuring placeholders
{0}/{1} and punctuation are preserved exactly.
- Around line 62-64: Translate the untranslated Korean strings in the
translations file packages/lib/translations/ko/web.po: locate the msgid entries
such as "\"{title}\" has been successfully hidden" (and the other untranslated
msgid ranges referenced: 4000-4002, 4001-4002 etc. as noted in the comment) and
provide appropriate Korean msgstr values for each untranslated UI, toast, admin
settings, audit log, and team-member guidance string so the Korean locale no
longer falls back to English; ensure placeholders like "{title}" are preserved
exactly in each msgstr and follow existing localization style used elsewhere in
web.po.
In `@packages/lib/translations/nl/web.po`:
- Around line 21-25: Several Dutch translations in
packages/lib/translations/nl/web.po are empty (e.g. the msgid " · {0} day{1}
remaining" used by
packages/email/template-components/template-sender-reminder-digest.tsx and many
other msgid blocks listed) causing fallbacks to English; open the nl web.po and
provide accurate Dutch msgstr entries for each empty msgid (preserving
placeholders {0}/{1} and pluralization logic), for example translate " · {0}
day{1} remaining" to a correct Dutch equivalent that respects singular/plural,
and repeat this for the other empty blocks referenced (lines ~62-64, 297-300,
339-341, 453-455, 801-815, 4000-4002, 5665-5667, 7382-7384, 8272-8321,
10026-10028, 10042-10044, 10804-10806, 10911-10913, 13519-13521), then run the
PO linter/CI to ensure formatting and placeholder usage are correct before
committing.
In `@packages/lib/translations/pl/web.po`:
- Around line 21-26: The Polish translation entries for the reminder/digest
strings are empty; locate the msgid " · {0} day{1} remaining" (used by
packages/email/template-components/template-sender-reminder-digest.tsx) and the
other identical reminder msgid occurrences listed in the comment and provide
appropriate Polish msgstr values (handling pluralization placeholder {1} so it
produces correct singular/plural in Polish), updating
packages/lib/translations/pl/web.po to replace each empty msgstr with the
correct Polish translation.
- Around line 10844-10846: The translated msgstr for the PO entry contains an
unintended "\n" and an extra empty string fragment; edit the msgstr in
packages/lib/translations/pl/web.po (the affected translation entry where msgstr
is split across two quoted lines) to remove the "\n" and the additional empty
string so the translation becomes a single continuous string without the
newline.
In `@packages/lib/translations/pt-BR/web.po`:
- Around line 16-20: The pt-BR translation entries for the new reminder/digest
strings are empty; update the msgstr for each affected msgid (e.g., " · {0}
day{1} remaining" from
packages/email/template-components/template-sender-reminder-digest.tsx and the
other identical msgid occurrences noted) with proper Portuguese (pt-BR)
translations preserving placeholders {0} and {1}; ensure you update all
corresponding msgstr blocks listed in this review (including the other ranges
called out) so the recipient reminder flow, owner digest, audit log text, and
new setting label no longer fall back to English.
- Around line 57-59: Fill the empty pt-BR msgstr values for the newly added UI
strings (e.g., the msgid "\"{title}\" has been successfully hidden") with
accurate Portuguese translations while preserving placeholders and surrounding
quotes (for example, use "\"{title}\" foi ocultado com sucesso" or another
grammatically correct variant), and do the same for the other new hide/duplicate
flow and team-member guidance msgid entries present in the same translations
file; ensure each msgstr preserves interpolation tokens like {title}, retains
punctuation/quotes, and matches context/tense of the original English strings.
In `@packages/lib/translations/zh/web.po`:
- Around line 21-25: Add Simplified Chinese translations for the new
reminder/digest/email/audit-log msgid entries (e.g., the msgid " · {0} day{1}
remaining" referenced in template-sender-reminder-digest.tsx and the other new
reminder strings mentioned) by filling each msgstr with the appropriate zh
translation while preserving placeholders {0}/{1} and any punctuation/spacing;
update all untranslated msgstr occurrences noted (including the other ranges
called out) so the zh PO catalog contains completed translations for every new
reminder/digest/audit-log string.
- Around line 62-64: The zh translation file is missing Chinese strings for
several new UI messages including the dialog message msgid "\"{title}\" has been
successfully hidden" (from envelope-delete-dialog.tsx); update the corresponding
msgstr to a proper Chinese translation (e.g. "已成功隐藏“{title}”") and likewise fill
in the other empty msgstr entries referenced in this PR (lines/groups around
4000-4002, 7382-7384, 10026-10028, 10042-10044, 10804-10806, 10911-10913,
13519-13521) with appropriate Chinese UI text matching the English msgids
(hide/duplicate flows, owner reminder digest setting, team-member guidance) so
all new dialog/admin/help strings are translated.
In
`@packages/prisma/migrations/20260416021003_add_document_reminder_log/migration.sql`:
- Around line 15-19: Add a composite index on DocumentReminderLog to optimize
reminder-sweep/digest queries that filter by envelopeId and recipientId and
order by newest timestamps: create an index on
("envelopeId","recipientId","createdAt" DESC) (e.g., name it
DocumentReminderLog_envelope_recipient_createdAt_idx) so queries against
DocumentReminderLog using envelopeId + recipientId with ORDER BY createdAt DESC
can use an index-only plan and avoid expensive sorts.
---
Outside diff comments:
In `@packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.ts`:
- Around line 9-20: The job payload schema
SEND_OWNER_REMINDER_DIGEST_EMAIL_JOB_DEFINITION_SCHEMA was changed to add a
required userId field but the job definition object
SEND_OWNER_REMINDER_DIGEST_EMAIL_JOB_DEFINITION still uses version "1.0.0";
update the version string (e.g., to "1.1.0" or a new semver per your project
policy) in SEND_OWNER_REMINDER_DIGEST_EMAIL_JOB_DEFINITION so that consumers and
retry systems can detect the breaking payload change and handle in-flight jobs
correctly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 25d46958-f0c0-4a0d-b95d-ad7c5bf7a21e
📒 Files selected for processing (16)
packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.handler.tspackages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.tspackages/lib/jobs/definitions/emails/send-recipient-reminder-email.handler.tspackages/lib/jobs/definitions/internal/send-reminders-sweep.handler.tspackages/lib/translations/de/web.popackages/lib/translations/en/web.popackages/lib/translations/es/web.popackages/lib/translations/fr/web.popackages/lib/translations/it/web.popackages/lib/translations/ja/web.popackages/lib/translations/ko/web.popackages/lib/translations/nl/web.popackages/lib/translations/pl/web.popackages/lib/translations/pt-BR/web.popackages/lib/translations/zh/web.popackages/prisma/migrations/20260416021003_add_document_reminder_log/migration.sql
| await io.runTask('create-reminder-logs', async () => { | ||
| await prisma.documentReminderLog.createMany({ | ||
| data: envelopeIds.map((eid) => ({ envelopeId: eid })), | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Create reminder logs only for envelopes included in the digest.
The createMany uses envelopeIds from the payload, but the query at line 28-42 filters envelopes by status: DocumentStatus.PENDING. If an envelope's status changed or it was deleted between the sweep and handler execution, this creates logs for documents not in the digest or causes a foreign key violation.
Use the queried envelopes to ensure logs are only created for documents actually included in the email.
🐛 Proposed fix
await io.runTask('create-reminder-logs', async () => {
await prisma.documentReminderLog.createMany({
- data: envelopeIds.map((eid) => ({ envelopeId: eid })),
+ data: envelopes.map((envelope) => ({ envelopeId: envelope.id })),
});
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await io.runTask('create-reminder-logs', async () => { | |
| await prisma.documentReminderLog.createMany({ | |
| data: envelopeIds.map((eid) => ({ envelopeId: eid })), | |
| }); | |
| }); | |
| await io.runTask('create-reminder-logs', async () => { | |
| await prisma.documentReminderLog.createMany({ | |
| data: envelopes.map((envelope) => ({ envelopeId: envelope.id })), | |
| }); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.handler.ts`
around lines 119 - 123, The createMany currently uses envelopeIds from the
payload which can include envelopes that were filtered out earlier; change the
log creation to use the actual queried envelopes collection (the envelopes
variable returned by the query) so only documents included in the digest get
logs. Inside the io.runTask('create-reminder-logs', ...) block, replace
prisma.documentReminderLog.createMany's data mapping from envelopeIds.map(...)
to envelopes.map(e => ({ envelopeId: e.id })) (or equivalent) and handle the
case of an empty envelopes array before calling createMany to avoid unnecessary
DB calls or FK errors.
| const envelope = await prisma.envelope.findFirst({ | ||
| where: { id: envelopeId }, | ||
| include: { | ||
| user: { | ||
| select: { id: true, email: true, name: true }, | ||
| }, | ||
| documentMeta: true, | ||
| team: { | ||
| select: { teamEmail: true, name: true, url: true }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| if (!envelope) { | ||
| throw new Error(`Envelope ${envelopeId} not found`); | ||
| } | ||
|
|
||
| if (envelope.status !== DocumentStatus.PENDING) { | ||
| return; | ||
| } | ||
|
|
||
| const recipient = await prisma.recipient.findFirst({ | ||
| where: { id: recipientId, envelopeId }, | ||
| }); | ||
|
|
||
| if (!recipient) { | ||
| throw new Error(`Recipient ${recipientId} not found on envelope ${envelopeId}`); | ||
| } | ||
|
|
||
| if (recipient.signingStatus !== SigningStatus.NOT_SIGNED) { | ||
| return; | ||
| } | ||
|
|
||
| const { branding, emailLanguage, senderEmail } = await getEmailContext({ | ||
| emailType: 'RECIPIENT', | ||
| source: { type: 'team', teamId: envelope.teamId }, | ||
| meta: envelope.documentMeta, | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider wrapping database reads in io.runTask for full idempotency compliance.
Per coding guidelines, all job handler steps should be wrapped in io.runTask. While read operations are naturally idempotent, wrapping them ensures consistent behavior if the job restarts mid-execution and prevents redundant queries on retry.
The write operations (lines 84-120) are correctly wrapped—this is the critical part. This is a minor consistency improvement.
♻️ Optional refactor to wrap reads
+ const envelope = await io.runTask('fetch-envelope', async () => {
+ return prisma.envelope.findFirst({
- const envelope = await prisma.envelope.findFirst({
where: { id: envelopeId },
include: {
user: {
select: { id: true, email: true, name: true },
},
documentMeta: true,
team: {
select: { teamEmail: true, name: true, url: true },
},
},
});
+ });Apply similar wrapping to the recipient query and getEmailContext call.
As per coding guidelines: "All job handler steps must be wrapped in io.runTask('unique-key', fn) for idempotency"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/lib/jobs/definitions/emails/send-recipient-reminder-email.handler.ts`
around lines 28 - 65, Wrap the read steps in the job handler inside io.runTask
to follow idempotency guidelines: call io.runTask with distinct keys (e.g.,
`load-envelope-${envelopeId}`, `load-recipient-${envelopeId}-${recipientId}`,
`email-context-${envelopeId}-${recipientId}`) and move the
prisma.envelope.findFirst, prisma.recipient.findFirst and getEmailContext
invocations into the respective runTask callbacks so each read is executed under
io.runTask; keep existing error checks (Envelope/Recipient not found) and return
logic unchanged inside those callbacks.
| const envelopes = await prisma.envelope.findMany({ | ||
| where: { | ||
| status: DocumentStatus.PENDING, | ||
| documentMeta: { | ||
| reminderEnabled: true, | ||
| reminderIntervalDays: { not: null }, | ||
| }, | ||
| }, | ||
| include: { | ||
| documentMeta: true, | ||
| recipients: { | ||
| where: { | ||
| signingStatus: { notIn: [SigningStatus.SIGNED, SigningStatus.REJECTED] }, | ||
| documentDeletedAt: null, | ||
| }, | ||
| }, | ||
| reminderLogs: { | ||
| orderBy: { createdAt: 'desc' as const }, | ||
| }, | ||
| }, | ||
| take: 1000, | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Wrap the database query in io.runTask for idempotency.
Per coding guidelines, all job handler steps should be wrapped. If the job fails after this query, the retry will re-execute the query unnecessarily. While reads are safe to retry, wrapping provides consistent checkpoint behavior.
♻️ Proposed refactor
- const envelopes = await prisma.envelope.findMany({
+ const envelopes = await io.runTask('fetch-due-envelopes', async () => {
+ return prisma.envelope.findMany({
where: {
status: DocumentStatus.PENDING,
documentMeta: {
reminderEnabled: true,
reminderIntervalDays: { not: null },
},
},
include: {
documentMeta: true,
recipients: {
where: {
signingStatus: { notIn: [SigningStatus.SIGNED, SigningStatus.REJECTED] },
documentDeletedAt: null,
},
},
reminderLogs: {
orderBy: { createdAt: 'desc' as const },
},
},
take: 1000,
});
+ });Based on learnings: "All job handler steps must be wrapped in io.runTask('unique-key', fn) for idempotency"
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const envelopes = await prisma.envelope.findMany({ | |
| where: { | |
| status: DocumentStatus.PENDING, | |
| documentMeta: { | |
| reminderEnabled: true, | |
| reminderIntervalDays: { not: null }, | |
| }, | |
| }, | |
| include: { | |
| documentMeta: true, | |
| recipients: { | |
| where: { | |
| signingStatus: { notIn: [SigningStatus.SIGNED, SigningStatus.REJECTED] }, | |
| documentDeletedAt: null, | |
| }, | |
| }, | |
| reminderLogs: { | |
| orderBy: { createdAt: 'desc' as const }, | |
| }, | |
| }, | |
| take: 1000, | |
| }); | |
| const envelopes = await io.runTask('fetch-due-envelopes', async () => { | |
| return prisma.envelope.findMany({ | |
| where: { | |
| status: DocumentStatus.PENDING, | |
| documentMeta: { | |
| reminderEnabled: true, | |
| reminderIntervalDays: { not: null }, | |
| }, | |
| }, | |
| include: { | |
| documentMeta: true, | |
| recipients: { | |
| where: { | |
| signingStatus: { notIn: [SigningStatus.SIGNED, SigningStatus.REJECTED] }, | |
| documentDeletedAt: null, | |
| }, | |
| }, | |
| reminderLogs: { | |
| orderBy: { createdAt: 'desc' as const }, | |
| }, | |
| }, | |
| take: 1000, | |
| }); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/lib/jobs/definitions/internal/send-reminders-sweep.handler.ts`
around lines 13 - 34, The initial Prisma query (prisma.envelope.findMany) in
send-reminders-sweep.handler should be executed inside an idempotent checkpoint
by wrapping it with io.runTask using a unique task key; change the code so the
call to prisma.envelope.findMany (including its where/include/take) is moved
into a function passed to io.runTask('send-reminders-sweep:fetch-envelopes',
async () => { ... }) so the database read becomes a managed task and the handler
follows the job-step wrapping convention.
| const recipientResults = await Promise.allSettled( | ||
| dueRecipients.map(async (r) => { | ||
| await jobs.triggerJob({ | ||
| name: 'send.recipient.reminder.email', | ||
| payload: { recipientId: r.id, envelopeId: r.envelopeId }, | ||
| }); | ||
| }), | ||
| ); | ||
|
|
||
| for (const result of recipientResults) { | ||
| if (result.status === 'rejected') { | ||
| io.logger.error('Failed to trigger recipient reminder', { reason: result.reason }); | ||
| } | ||
| } | ||
|
|
||
| const digestResults = await Promise.allSettled( | ||
| Array.from(dueEnvelopesByOwner.values()).map(async ({ teamId, userId, envelopeIds }) => { | ||
| await jobs.triggerJob({ | ||
| name: 'send.owner.reminder.digest.email', | ||
| payload: { teamId, userId, envelopeIds: Array.from(envelopeIds) }, | ||
| }); | ||
| }), | ||
| ); | ||
|
|
||
| for (const result of digestResults) { | ||
| if (result.status === 'rejected') { | ||
| io.logger.error('Failed to trigger owner digest', { reason: result.reason }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Job triggers are not wrapped in io.runTask, risking duplicate jobs on retry.
If this sweep job fails after triggering some recipient/digest jobs but before completing, a retry will re-trigger ALL jobs—including ones that already ran. Each triggered job is a separate instance with its own idempotency scope.
Wrap each trigger in io.runTask with a unique key (e.g., recipient ID) to prevent duplicate triggers.
🔧 Proposed fix for recipient job triggers
const recipientResults = await Promise.allSettled(
dueRecipients.map(async (r) => {
- await jobs.triggerJob({
- name: 'send.recipient.reminder.email',
- payload: { recipientId: r.id, envelopeId: r.envelopeId },
+ await io.runTask(`trigger-recipient-${r.id}`, async () => {
+ await jobs.triggerJob({
+ name: 'send.recipient.reminder.email',
+ payload: { recipientId: r.id, envelopeId: r.envelopeId },
+ });
});
}),
);Apply similar wrapping for the digest job triggers using io.runTask(\trigger-digest-${ownerKey}`, ...)`.
Based on learnings: "All job handler steps must be wrapped in io.runTask('unique-key', fn) for idempotency"
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const recipientResults = await Promise.allSettled( | |
| dueRecipients.map(async (r) => { | |
| await jobs.triggerJob({ | |
| name: 'send.recipient.reminder.email', | |
| payload: { recipientId: r.id, envelopeId: r.envelopeId }, | |
| }); | |
| }), | |
| ); | |
| for (const result of recipientResults) { | |
| if (result.status === 'rejected') { | |
| io.logger.error('Failed to trigger recipient reminder', { reason: result.reason }); | |
| } | |
| } | |
| const digestResults = await Promise.allSettled( | |
| Array.from(dueEnvelopesByOwner.values()).map(async ({ teamId, userId, envelopeIds }) => { | |
| await jobs.triggerJob({ | |
| name: 'send.owner.reminder.digest.email', | |
| payload: { teamId, userId, envelopeIds: Array.from(envelopeIds) }, | |
| }); | |
| }), | |
| ); | |
| for (const result of digestResults) { | |
| if (result.status === 'rejected') { | |
| io.logger.error('Failed to trigger owner digest', { reason: result.reason }); | |
| } | |
| } | |
| const recipientResults = await Promise.allSettled( | |
| dueRecipients.map(async (r) => { | |
| await io.runTask(`trigger-recipient-${r.id}`, async () => { | |
| await jobs.triggerJob({ | |
| name: 'send.recipient.reminder.email', | |
| payload: { recipientId: r.id, envelopeId: r.envelopeId }, | |
| }); | |
| }); | |
| }), | |
| ); | |
| for (const result of recipientResults) { | |
| if (result.status === 'rejected') { | |
| io.logger.error('Failed to trigger recipient reminder', { reason: result.reason }); | |
| } | |
| } | |
| const digestResults = await Promise.allSettled( | |
| Array.from(dueEnvelopesByOwner.values()).map(async ({ teamId, userId, envelopeIds }) => { | |
| await jobs.triggerJob({ | |
| name: 'send.owner.reminder.digest.email', | |
| payload: { teamId, userId, envelopeIds: Array.from(envelopeIds) }, | |
| }); | |
| }), | |
| ); | |
| for (const result of digestResults) { | |
| if (result.status === 'rejected') { | |
| io.logger.error('Failed to trigger owner digest', { reason: result.reason }); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/lib/jobs/definitions/internal/send-reminders-sweep.handler.ts`
around lines 93 - 121, The recipient and digest job triggers must be wrapped in
io.runTask to ensure idempotency; replace the direct calls to jobs.triggerJob
inside the dueRecipients map and the dueEnvelopesByOwner map with
io.runTask(...) that returns the jobs.triggerJob promise, using unique keys such
as `trigger-recipient-${r.id}-${r.envelopeId}` for recipients and
`trigger-digest-${teamId}-${userId}` (or
`trigger-digest-${teamId}-${userId}-${ownerKey}`) for owner digests so
Promise.allSettled operates on runTask results and prevents duplicate triggers
on retries.
| #. placeholder {0}: doc.daysRemaining | ||
| #. placeholder {1}: doc.daysRemaining === 1 ? '' : 's' | ||
| #: packages/email/template-components/template-sender-reminder-digest.tsx | ||
| msgid " · {0} day{1} remaining" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd 'template-sender-reminder-digest\.tsx|document-reminder\.tsx|template-document-reminder\.tsx' . | while read -r f; do
echo "== $f =="
rg -n -C3 "daysRemaining === 1|doc\.daysRemaining === 1|plural\(" "$f" || true
doneRepository: b-mackenzie-alexander/documenso
Length of output: 1310
Use ICU plural forms instead of English suffix placeholders in source messages.
The source components (template-document-reminder.tsx, template-sender-reminder-digest.tsx, document-reminder.tsx) use ternary operators to append English grammar (day{daysRemaining === 1 ? '' : 's'}). This pattern bakes English pluralization into the extracted messages, preventing German translators from applying correct plural forms. Change these to use Lingui's plural() helper or equivalent ICU syntax so that locale-specific rules apply during translation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/lib/translations/de/web.po` around lines 21 - 24, The extracted
source messages bake English plural suffixes into the text (e.g. the msgid " ·
{0} day{1} remaining") from usages in template-document-reminder.tsx,
template-sender-reminder-digest.tsx, and document-reminder.tsx; replace the
ternary/placeholder pattern with Lingui/ICU plural syntax (or Lingui.plural())
in each component so the extractor emits proper plural forms (e.g. use a single
plural expression keyed on daysRemaining instead of separate {0}/{1}
placeholders), then regenerate the catalog so web.po contains ICU plural entries
instead of English-only suffix placeholders. Ensure you update the source calls
that render the message to pass daysRemaining as the plural variable and remove
the separate suffix placeholder usage.
| #. placeholder {0}: doc.daysRemaining | ||
| #. placeholder {1}: doc.daysRemaining === 1 ? '' : 's' | ||
| #: packages/email/template-components/template-sender-reminder-digest.tsx | ||
| msgid " · {0} day{1} remaining" | ||
| msgstr "" |
There was a problem hiding this comment.
Translate the new reminder and digest copy before release.
These new pt-BR entries are still empty, so the recipient reminder flow, owner digest, audit log text, and the new setting label will all fall back to English for Brazilian users.
Suggested pt-BR translations
msgid " · {0} day{1} remaining"
-msgstr ""
+msgstr " · {0} dia{1} restante{1}"
msgid "{0} recipients pending"
-msgstr ""
+msgstr "{0} destinatários pendentes"
msgid "{count} documents in {teamName} are awaiting signatures"
-msgstr ""
+msgstr "{count} documentos em {teamName} estão aguardando assinaturas"
msgid "{senderName} is waiting for your signature."
-msgstr ""
+msgstr "{senderName} está aguardando sua assinatura."
msgid "1 document in {teamName} is awaiting a signature"
-msgstr ""
+msgstr "1 documento em {teamName} está aguardando uma assinatura"
msgid "1 recipient pending"
-msgstr ""
+msgstr "1 destinatário pendente"
msgid "Hi {ownerName}, here's a summary of documents that still need attention."
-msgstr ""
+msgstr "Olá {ownerName}, aqui está um resumo dos documentos que ainda precisam de atenção."
msgid "Owner reminder digest"
-msgstr ""
+msgstr "Resumo de lembretes do proprietário"
msgid "Reminder sent to {0}"
-msgstr ""
+msgstr "Lembrete enviado para {0}"
msgid "Reminder: {count} documents in {teamName} are awaiting signatures"
-msgstr ""
+msgstr "Lembrete: {count} documentos em {teamName} estão aguardando assinaturas"
msgid "Reminder: 1 document in {teamName} is awaiting a signature"
-msgstr ""
+msgstr "Lembrete: 1 documento em {teamName} está aguardando uma assinatura"
msgid "Reminder: you have {daysRemaining} day{0} left to sign \"{documentName}\""
-msgstr ""
+msgstr "Lembrete: você tem {daysRemaining} dia{0} restante{0} para assinar \"{documentName}\""
msgid "Reminder: you have {daysRemaining} day{0} left to sign<0/>\"{documentName}\""
-msgstr ""
+msgstr "Lembrete: você tem {daysRemaining} dia{0} restante{0} para assinar<0/>\"{documentName}\""
msgid "Reminder: your signature is still needed on \"{documentName}\""
-msgstr ""
+msgstr "Lembrete: sua assinatura ainda é necessária no documento \"{documentName}\""
msgid "Reminder: your signature is still needed on<0/>\"{documentName}\""
-msgstr ""
+msgstr "Lembrete: sua assinatura ainda é necessária no documento<0/>\"{documentName}\""Also applies to: 292-295, 334-336, 448-450, 796-810, 5660-5662, 7377-7379, 8267-8316
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/lib/translations/pt-BR/web.po` around lines 16 - 20, The pt-BR
translation entries for the new reminder/digest strings are empty; update the
msgstr for each affected msgid (e.g., " · {0} day{1} remaining" from
packages/email/template-components/template-sender-reminder-digest.tsx and the
other identical msgid occurrences noted) with proper Portuguese (pt-BR)
translations preserving placeholders {0} and {1}; ensure you update all
corresponding msgstr blocks listed in this review (including the other ranges
called out) so the recipient reminder flow, owner digest, audit log text, and
new setting label no longer fall back to English.
| #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx | ||
| msgid "\"{title}\" has been successfully hidden" | ||
| msgstr "" |
There was a problem hiding this comment.
Fill in the remaining newly added pt-BR UI strings.
These entries are also empty and will surface English in updated hide/duplicate flows and the team-member guidance copy.
Suggested pt-BR translations
msgid "\"{title}\" has been successfully hidden"
-msgstr ""
+msgstr "\"{title}\" foi ocultado com sucesso"
msgid "Document hidden"
-msgstr ""
+msgstr "Documento oculto"
msgid "Template Duplicated"
-msgstr ""
+msgstr "Modelo duplicado"
msgid "Template hidden"
-msgstr ""
+msgstr "Modelo oculto"
msgid "This template could not be duplicated at this time. Please try again."
-msgstr ""
+msgstr "Não foi possível duplicar este modelo neste momento. Por favor, tente novamente."
msgid "To add members to this team, they must first be invited to the organisation. Only organisation admins and managers can invite new members — please contact one of them to invite members on your behalf."
-msgstr ""
+msgstr "Para adicionar membros a esta equipe, eles primeiro precisam ser convidados para a organização. Apenas administradores e gerentes da organização podem convidar novos membros — entre em contato com um deles para convidá-los em seu nome."
msgid "Your template has been successfully duplicated."
-msgstr ""
+msgstr "Seu modelo foi duplicado com sucesso."Also applies to: 3995-3997, 10021-10023, 10037-10039, 10799-10801, 10906-10908, 13514-13516
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/lib/translations/pt-BR/web.po` around lines 57 - 59, Fill the empty
pt-BR msgstr values for the newly added UI strings (e.g., the msgid "\"{title}\"
has been successfully hidden") with accurate Portuguese translations while
preserving placeholders and surrounding quotes (for example, use "\"{title}\"
foi ocultado com sucesso" or another grammatically correct variant), and do the
same for the other new hide/duplicate flow and team-member guidance msgid
entries present in the same translations file; ensure each msgstr preserves
interpolation tokens like {title}, retains punctuation/quotes, and matches
context/tense of the original English strings.
| #. placeholder {0}: doc.daysRemaining | ||
| #. placeholder {1}: doc.daysRemaining === 1 ? '' : 's' | ||
| #: packages/email/template-components/template-sender-reminder-digest.tsx | ||
| msgid " · {0} day{1} remaining" | ||
| msgstr "" |
There was a problem hiding this comment.
Translate the new reminder copy before shipping the zh catalog.
All of the new reminder/digest/email/audit-log entries added for this feature still have empty msgstr values, so the new reminder experience will ship untranslated in Simplified Chinese.
Suggested zh translations for the load-bearing reminder strings
msgid " · {0} day{1} remaining"
-msgstr ""
+msgstr " · 剩余 {0} 天"
msgid "{0} recipients pending"
-msgstr ""
+msgstr "还有 {0} 位收件人待处理"
msgid "{count} documents in {teamName} are awaiting signatures"
-msgstr ""
+msgstr "{teamName} 中有 {count} 份文档正在等待签署"
msgid "1 document in {teamName} is awaiting a signature"
-msgstr ""
+msgstr "{teamName} 中有 1 份文档正在等待签署"
msgid "1 recipient pending"
-msgstr ""
+msgstr "还有 1 位收件人待处理"
msgid "{senderName} is waiting for your signature."
-msgstr ""
+msgstr "{senderName} 正在等待您的签名。"
msgid "Hi {ownerName}, here's a summary of documents that still need attention."
-msgstr ""
+msgstr "您好 {ownerName},以下是仍需处理的文档摘要。"
msgid "Reminder sent to {0}"
-msgstr ""
+msgstr "提醒已发送给 {0}"
msgid "Reminder: {count} documents in {teamName} are awaiting signatures"
-msgstr ""
+msgstr "提醒:{teamName} 中有 {count} 份文档正在等待签署"
msgid "Reminder: 1 document in {teamName} is awaiting a signature"
-msgstr ""
+msgstr "提醒:{teamName} 中有 1 份文档正在等待签署"
msgid "Reminder: you have {daysRemaining} day{0} left to sign \"{documentName}\""
-msgstr ""
+msgstr "提醒:您还有 {daysRemaining} 天可签署“{documentName}”"
msgid "Reminder: you have {daysRemaining} day{0} left to sign<0/>\"{documentName}\""
-msgstr ""
+msgstr "提醒:您还有 {daysRemaining} 天可签署<0/>“{documentName}”"
msgid "Reminder: your signature is still needed on \"{documentName}\""
-msgstr ""
+msgstr "提醒:您仍需在“{documentName}”上签名"
msgid "Reminder: your signature is still needed on<0/>\"{documentName}\""
-msgstr ""
+msgstr "提醒:您仍需在<0/>“{documentName}”上签名"Also applies to: 297-300, 339-341, 453-455, 801-815, 5665-5667, 8272-8317
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/lib/translations/zh/web.po` around lines 21 - 25, Add Simplified
Chinese translations for the new reminder/digest/email/audit-log msgid entries
(e.g., the msgid " · {0} day{1} remaining" referenced in
template-sender-reminder-digest.tsx and the other new reminder strings
mentioned) by filling each msgstr with the appropriate zh translation while
preserving placeholders {0}/{1} and any punctuation/spacing; update all
untranslated msgstr occurrences noted (including the other ranges called out) so
the zh PO catalog contains completed translations for every new
reminder/digest/audit-log string.
| #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx | ||
| msgid "\"{title}\" has been successfully hidden" | ||
| msgstr "" |
There was a problem hiding this comment.
Fill in the remaining new zh UI strings touched by this PR.
These new dialog/admin/help entries are also still empty, so the hide/duplicate flows, the owner reminder digest setting, and the new team-member guidance will ship untranslated in Chinese.
Suggested zh translations for the missing UI strings
msgid "\"{title}\" has been successfully hidden"
-msgstr ""
+msgstr "“{title}”已成功隐藏"
msgid "Document hidden"
-msgstr ""
+msgstr "文档已隐藏"
msgid "Owner reminder digest"
-msgstr ""
+msgstr "所有者提醒摘要"
msgid "Template Duplicated"
-msgstr ""
+msgstr "模板已复制"
msgid "Template hidden"
-msgstr ""
+msgstr "模板已隐藏"
msgid "This template could not be duplicated at this time. Please try again."
-msgstr ""
+msgstr "当前无法复制此模板。请重试。"
msgid "To add members to this team, they must first be invited to the organisation. Only organisation admins and managers can invite new members — please contact one of them to invite members on your behalf."
-msgstr ""
+msgstr "要将成员添加到此团队,必须先将他们邀请到组织中。只有组织管理员和管理者可以邀请新成员——请联系其中一位代您邀请成员。"
msgid "Your template has been successfully duplicated."
-msgstr ""
+msgstr "您的模板已成功复制。"Also applies to: 4000-4002, 7382-7384, 10026-10028, 10042-10044, 10804-10806, 10911-10913, 13519-13521
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/lib/translations/zh/web.po` around lines 62 - 64, The zh translation
file is missing Chinese strings for several new UI messages including the dialog
message msgid "\"{title}\" has been successfully hidden" (from
envelope-delete-dialog.tsx); update the corresponding msgstr to a proper Chinese
translation (e.g. "已成功隐藏“{title}”") and likewise fill in the other empty msgstr
entries referenced in this PR (lines/groups around 4000-4002, 7382-7384,
10026-10028, 10042-10044, 10804-10806, 10911-10913, 13519-13521) with
appropriate Chinese UI text matching the English msgids (hide/duplicate flows,
owner reminder digest setting, team-member guidance) so all new
dialog/admin/help strings are translated.
| -- CreateIndex | ||
| CREATE INDEX "DocumentReminderLog_envelopeId_idx" ON "DocumentReminderLog"("envelopeId"); | ||
|
|
||
| -- CreateIndex | ||
| CREATE INDEX "DocumentReminderLog_recipientId_idx" ON "DocumentReminderLog"("recipientId"); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider a composite index for reminder-log lookup hot paths.
Single-column indexes are fine, but reminder sweep/digest reads usually filter by envelope/recipient and need newest timestamps. Add a composite index to avoid expensive sort/filter plans at scale.
Suggested migration addition
-- CreateIndex
CREATE INDEX "DocumentReminderLog_envelopeId_idx" ON "DocumentReminderLog"("envelopeId");
-- CreateIndex
CREATE INDEX "DocumentReminderLog_recipientId_idx" ON "DocumentReminderLog"("recipientId");
+
+-- CreateIndex
+CREATE INDEX "DocumentReminderLog_envelopeId_recipientId_createdAt_idx"
+ON "DocumentReminderLog"("envelopeId", "recipientId", "createdAt" DESC);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| -- CreateIndex | |
| CREATE INDEX "DocumentReminderLog_envelopeId_idx" ON "DocumentReminderLog"("envelopeId"); | |
| -- CreateIndex | |
| CREATE INDEX "DocumentReminderLog_recipientId_idx" ON "DocumentReminderLog"("recipientId"); | |
| -- CreateIndex | |
| CREATE INDEX "DocumentReminderLog_envelopeId_idx" ON "DocumentReminderLog"("envelopeId"); | |
| -- CreateIndex | |
| CREATE INDEX "DocumentReminderLog_recipientId_idx" ON "DocumentReminderLog"("recipientId"); | |
| -- CreateIndex | |
| CREATE INDEX "DocumentReminderLog_envelopeId_recipientId_createdAt_idx" | |
| ON "DocumentReminderLog"("envelopeId", "recipientId", "createdAt" DESC); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/prisma/migrations/20260416021003_add_document_reminder_log/migration.sql`
around lines 15 - 19, Add a composite index on DocumentReminderLog to optimize
reminder-sweep/digest queries that filter by envelopeId and recipientId and
order by newest timestamps: create an index on
("envelopeId","recipientId","createdAt" DESC) (e.g., name it
DocumentReminderLog_envelope_recipient_createdAt_idx) so queries against
DocumentReminderLog using envelopeId + recipientId with ORDER BY createdAt DESC
can use an index-only plan and avoid expensive sorts.
Addresses review findings on send-owner-reminder-digest-email.handler: - Destructures userId from payload (added to schema in companion sweep fix) - Adds status: PENDING, teamId, userId filters to findMany — completed documents no longer appear in the digest, and each batch is scoped to the correct owner - Fixes subject pluralization: replaces ternary with Lingui plural() for proper i18n plural support across locales Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0fbf583 to
6ce3fa6
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.handler.ts (1)
48-62:⚠️ Potential issue | 🟠 MajorFilter digest-eligible envelopes before picking
firstEnvelope.
findManyhas noorderBy, soenvelopes[0]is arbitrary. IfownerReminderDigestor email metadata differ across documents, this can nondeterministically send/skip the digest, render it with the wrong context, and create reminder logs for envelopes that were never actually included. Build a filtereddigestEnvelopeslist first, then use that list for context,pendingDocuments,count, and log creation.♻️ Possible fix
- const firstEnvelope = envelopes[0]; - - const isDigestEnabled = extractDerivedDocumentEmailSettings( - firstEnvelope.documentMeta, - ).ownerReminderDigest; - - if (!isDigestEnabled) { + const digestEnvelopes = envelopes.filter((envelope) => + extractDerivedDocumentEmailSettings(envelope.documentMeta).ownerReminderDigest, + ); + + if (digestEnvelopes.length === 0) { return; } + + const firstEnvelope = digestEnvelopes[0]; @@ - const pendingDocuments = envelopes.map((envelope) => { + const pendingDocuments = digestEnvelopes.map((envelope) => { @@ - const count = envelopes.length; + const count = digestEnvelopes.length; @@ - data: envelopeIds.map((eid) => ({ envelopeId: eid })), + data: digestEnvelopes.map((envelope) => ({ envelopeId: envelope.id })),Also applies to: 66-96, 119-123
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.handler.ts` around lines 48 - 62, The code chooses firstEnvelope from envelopes[0] before filtering, which can pick a document without ownerReminderDigest and lead to wrong context and logs; change the flow to first build a filtered digestEnvelopes = envelopes.filter(e => extractDerivedDocumentEmailSettings(e.documentMeta).ownerReminderDigest) (or equivalent predicate that also ensures matching email metadata), then use digestEnvelopes (not envelopes) for selecting the firstEnvelope for context (passed to getEmailContext), computing pendingDocuments/count, and creating reminder logs; update all uses at send-owner-reminder-digest-email.handler.ts that currently reference envelopes, firstEnvelope, pendingDocuments, count, or log creation so they operate on digestEnvelopes instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.handler.ts`:
- Around line 28-64: Wrap the initial database read and context lookups in
separate io.runTask calls to ensure idempotency: move the
prisma.envelope.findMany call into io.runTask('read-envelopes', ...) and
return/set envelopes, then run io.runTask('compute-digest-setting', ...) to call
extractDerivedDocumentEmailSettings(firstEnvelope.documentMeta) and compute
isDigestEnabled (return early if false inside that task), and run
io.runTask('get-email-context', ...) to call getEmailContext({ emailType:
'INTERNAL', source: { type: 'team', teamId }, meta: firstEnvelope.documentMeta
}) to obtain branding, emailLanguage, senderEmail, plus io.runTask('get-i18n',
...) to call getI18nInstance(emailLanguage) and set i18n; ensure you use the
same variable names (envelopes, firstEnvelope, isDigestEnabled, branding,
emailLanguage, senderEmail, i18n) so downstream code remains unchanged.
In
`@packages/lib/jobs/definitions/emails/send-recipient-reminder-email.handler.ts`:
- Around line 41-54: Replace the plain Error throws in
send-recipient-reminder-email.handler (the envelope not-found and recipient
not-found branches) with the project's AppError variant so they participate in
the jobs' typed error-handling; specifically, where you currently throw new
Error(`Envelope ${envelopeId} not found`) and new Error(`Recipient
${recipientId} not found on envelope ${envelopeId}`), throw new AppError(...)
with an appropriate not-found code/message, and ensure AppError is imported into
this module; keep the existing checks around envelope, envelope.status
(DocumentStatus.PENDING), and the prisma.recipient.findFirst call unchanged.
---
Duplicate comments:
In
`@packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.handler.ts`:
- Around line 48-62: The code chooses firstEnvelope from envelopes[0] before
filtering, which can pick a document without ownerReminderDigest and lead to
wrong context and logs; change the flow to first build a filtered
digestEnvelopes = envelopes.filter(e =>
extractDerivedDocumentEmailSettings(e.documentMeta).ownerReminderDigest) (or
equivalent predicate that also ensures matching email metadata), then use
digestEnvelopes (not envelopes) for selecting the firstEnvelope for context
(passed to getEmailContext), computing pendingDocuments/count, and creating
reminder logs; update all uses at send-owner-reminder-digest-email.handler.ts
that currently reference envelopes, firstEnvelope, pendingDocuments, count, or
log creation so they operate on digestEnvelopes instead.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 8ab97be3-36c7-4444-bbf2-a8bdcd4fab43
📒 Files selected for processing (2)
packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.handler.tspackages/lib/jobs/definitions/emails/send-recipient-reminder-email.handler.ts
| const envelopes = await prisma.envelope.findMany({ | ||
| where: { id: { in: envelopeIds }, teamId, userId, status: DocumentStatus.PENDING }, | ||
| include: { | ||
| user: { | ||
| select: { id: true, email: true, name: true }, | ||
| }, | ||
| documentMeta: true, | ||
| team: { | ||
| select: { name: true, url: true }, | ||
| }, | ||
| recipients: { | ||
| select: { signingStatus: true, expiresAt: true }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| if (envelopes.length === 0) { | ||
| return; | ||
| } | ||
|
|
||
| const firstEnvelope = envelopes[0]; | ||
|
|
||
| const isDigestEnabled = extractDerivedDocumentEmailSettings( | ||
| firstEnvelope.documentMeta, | ||
| ).ownerReminderDigest; | ||
|
|
||
| if (!isDigestEnabled) { | ||
| return; | ||
| } | ||
|
|
||
| const { branding, emailLanguage, senderEmail } = await getEmailContext({ | ||
| emailType: 'INTERNAL', | ||
| source: { type: 'team', teamId }, | ||
| meta: firstEnvelope.documentMeta, | ||
| }); | ||
|
|
||
| const i18n = await getI18nInstance(emailLanguage); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Wrap the read/context setup in io.runTask.
The initial Prisma read and the getEmailContext / getI18nInstance calls are still outside io.runTask, so a retry can observe a different state than the already-cached send/log tasks. Give these steps distinct task keys as well.
As per coding guidelines: "All job handler steps must be wrapped in io.runTask('unique-key', fn) for idempotency"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.handler.ts`
around lines 28 - 64, Wrap the initial database read and context lookups in
separate io.runTask calls to ensure idempotency: move the
prisma.envelope.findMany call into io.runTask('read-envelopes', ...) and
return/set envelopes, then run io.runTask('compute-digest-setting', ...) to call
extractDerivedDocumentEmailSettings(firstEnvelope.documentMeta) and compute
isDigestEnabled (return early if false inside that task), and run
io.runTask('get-email-context', ...) to call getEmailContext({ emailType:
'INTERNAL', source: { type: 'team', teamId }, meta: firstEnvelope.documentMeta
}) to obtain branding, emailLanguage, senderEmail, plus io.runTask('get-i18n',
...) to call getI18nInstance(emailLanguage) and set i18n; ensure you use the
same variable names (envelopes, firstEnvelope, isDigestEnabled, branding,
emailLanguage, senderEmail, i18n) so downstream code remains unchanged.
| if (!envelope) { | ||
| throw new Error(`Envelope ${envelopeId} not found`); | ||
| } | ||
|
|
||
| if (envelope.status !== DocumentStatus.PENDING) { | ||
| return; | ||
| } | ||
|
|
||
| const recipient = await prisma.recipient.findFirst({ | ||
| where: { id: recipientId, envelopeId }, | ||
| }); | ||
|
|
||
| if (!recipient) { | ||
| throw new Error(`Recipient ${recipientId} not found on envelope ${envelopeId}`); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use AppError for these not-found branches.
Both branches throw plain Error, which bypasses the repo’s typed error-handling contract for jobs. Replace them with the project’s AppError variant for not-found failures.
As per coding guidelines: "Use custom AppError class when throwing errors"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/lib/jobs/definitions/emails/send-recipient-reminder-email.handler.ts`
around lines 41 - 54, Replace the plain Error throws in
send-recipient-reminder-email.handler (the envelope not-found and recipient
not-found branches) with the project's AppError variant so they participate in
the jobs' typed error-handling; specifically, where you currently throw new
Error(`Envelope ${envelopeId} not found`) and new Error(`Recipient
${recipientId} not found on envelope ${envelopeId}`), throw new AppError(...)
with an appropriate not-found code/message, and ensure AppError is imported into
this module; keep the existing checks around envelope, envelope.status
(DocumentStatus.PENDING), and the prisma.recipient.findFirst call unchanged.
Summary
send-recipient-reminder-email.handler.ts— sends a reminder email to each unsigned recipient, creates aDocumentReminderLogentry and aREMINDER_SENTaudit log per recipientsend-owner-reminder-digest-email.handler.ts— sends a per-owner digest of pending documents to the document creator, createsDocumentReminderLogentries (nullrecipientId) per envelopeReview fixes applied (pre-review by @b-mackenzie-alexander)
findManynow filtersstatus: PENDING+teamId+userId— completed documents no longer appear in the digest(teamId, userId)instead ofteamId— each document creator receives their own digest scoped to their own documents (companion commit onfeat/doc-2147-schema-job-infrastructure)plural()instead of a ternaryDependencies
feat/doc-2147-schema-job-infrastructure). This branch is rebased on top of it — the diff includes Branch 1 commits. Retarget tomainand re-review diff after #12 lands.Runtime dependency: Person 1's migration (
20260416021003_add_document_reminder_log) must be applied before these handlers can write toDocumentReminderLog.Test plan
DocumentReminderLogrow created,REMINDER_SENTaudit log createdownerReminderDigestsetting disabled → digest handler returns earlyfindManyreturns empty, digest handler returns earlyDocumentReminderLogrow per envelope (nullrecipientId)🤖 Generated with Claude Code
Summary by CodeRabbit