Skip to content

feat: Person 1 — schema, migration, and sweep handler for document reminders#12

Merged
b-mackenzie-alexander merged 2 commits into
mainfrom
feat/doc-2147-schema-job-infrastructure
Apr 19, 2026
Merged

feat: Person 1 — schema, migration, and sweep handler for document reminders#12
b-mackenzie-alexander merged 2 commits into
mainfrom
feat/doc-2147-schema-job-infrastructure

Conversation

@OasisView
Copy link
Copy Markdown
Collaborator

@OasisView OasisView commented Apr 18, 2026

Summary

  • Prisma schema: Added reminderEnabled and reminderIntervalDays to DocumentMeta, plus new DocumentReminderLog model with back-relations to Envelope and Recipient
  • Migration: Additive-only — no destructive changes, no NOT NULL on existing columns
  • Sweep handler: Implemented send-reminders-sweep.handler.ts — cron job queries pending envelopes with reminders enabled, checks DocumentReminderLog for elapsed intervals, fans out send.recipient.reminder.email and send.owner.reminder.digest.email jobs (capped at 1,000 recipients per run, all steps wrapped in io.runTask() for idempotency)
  • Email settings: Added ownerReminderDigest to ZDocumentEmailSettingsSchema, DocumentEmailEvents enum, and DEFAULT_DOCUMENT_EMAIL_SETTINGS
  • Build-compat fixes (out of Person 1 scope, but required for npm run build):
    • document-email.ts: added ownerReminderDigest to non-EMAIL distribution fallback
    • document-audit-logs.ts: added REMINDER_SENT exhaustive match case
    • document.ts: added reminderEnabled/reminderIntervalDays to extractDerivedDocumentMeta
    • admin-global-settings-section.tsx: added ownerReminderDigest label to settings record

Scope

Person 1 — Schema + Job Infrastructure (see ROADMAP.md). Three out-of-scope files were touched with minimal 1-line fixes to resolve type errors caused by the schema additions propagating through the type system. These are documented above.

Dependencies

  • Unblocks Person 2 (email dispatch handlers) and Person 3 (email templates)
  • Person 4 (frontend) should coordinate rebase for document-email.ts changes

Test plan

  • npm run build passes (all 5 workspace tasks successful)
  • Migration is additive only — verified migration.sql contains no DROP, no NOT NULL on existing columns
  • Lint and prettier hooks pass
  • End-to-end test after Person 2 + Person 3 branches land: send document with reminders enabled → wait one interval → confirm recipient email fires → confirm sender digest fires

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Implemented reminder sweep that queues per-recipient reminders and owner digest emails.
  • Database
    • Added persistent tracking for document reminders to support scheduling and logs.
  • Chores
    • Updated owner-digest job payload to include owner identifier for delivery.
  • Translations
    • Added/updated translation strings for reminder emails and envelope hide/duplicate messaging across multiple languages.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

Implements the reminder sweep: queries envelopes with reminders enabled, computes due recipient/envelope pairs by interval and reminder logs, triggers per-recipient reminder jobs and per-owner digest jobs, adds DB migration for reminder logs, and adds related i18n strings across multiple locales.

Changes

Cohort / File(s) Summary
Reminder Sweep Handler
packages/lib/jobs/definitions/internal/send-reminders-sweep.handler.ts
Implemented run to fetch up to 1000 envelopes with documentMeta.reminderEnabled=true, include eligible recipients and reminder logs, compute days elapsed vs reminderIntervalDays, select up to 1000 due recipient/envelope pairs, group envelopes by {teamId,userId} for owner digests, and trigger recipient reminder and owner-digest jobs via Promise.allSettled with error logging.
Database Migration
packages/prisma/migrations/20260416021003_add_document_reminder_log/migration.sql
Added reminderEnabled BOOLEAN NOT NULL DEFAULT false and reminderIntervalDays INTEGER to DocumentMeta; created DocumentReminderLog table (id, createdAt, envelopeId, recipientId) with indexes and foreign keys (cascade on envelope, set null on recipient).
Job Definition Payload
packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.ts
Updated job schema/type to include userId: number in the SEND_OWNER_REMINDER_DIGEST_EMAIL_JOB_DEFINITION_SCHEMA / exported TSendOwnerReminderDigestEmailJobDefinition.
Translation Catalogs
packages/lib/translations/{de,en,es,fr,it,ja,ko,nl,pl,pt-BR,zh}/web.po
Added many new i18n msgids for reminder/digest emails, audit logs, and envelope visibility/duplication UI strings; removed or re-scoped several document/template dialog msgid source references toward envelope dialogs; many new non-English msgstr entries left empty.

Sequence Diagram

sequenceDiagram
    participant Sweep as Reminder Sweep Handler
    participant DB as Database
    participant Queue as Job Queue
    participant Email as Email Service

    Sweep->>DB: Query envelopes where reminderEnabled=true (include recipients, reminderLogs) (limit 1000)
    activate DB
    DB-->>Sweep: Return envelopes, recipients, ordered reminderLogs
    deactivate DB

    Sweep->>Sweep: Compute now, determine last reminder per recipient (log or envelope.createdAt)
    Sweep->>Sweep: Calculate days elapsed vs reminderIntervalDays, select due pairs (cap 1000)
    Sweep->>Sweep: Group due envelopeIds by {teamId,userId} for owner digests

    Sweep->>Queue: Enqueue per-recipient reminder jobs (Promise.allSettled)
    activate Queue
    Queue->>Email: Deliver reminder emails to recipients
    Email-->>Queue: Delivery status
    Queue-->>Sweep: Job trigger results
    deactivate Queue

    Sweep->>Queue: Enqueue per-owner digest jobs (Promise.allSettled)
    activate Queue
    Queue->>Email: Deliver owner digest emails
    Email-->>Queue: Delivery status
    Queue-->>Sweep: Job trigger results
    deactivate Queue

    Sweep->>Sweep: Log any job trigger rejections/errors
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through tables, logs, and queues tonight,
Found sleepy recipients, measured days just right,
Queued tiny nudges and owner digests too,
Schemas grew a log so nothing's missed or new,
Now signatures stir — a rabbit's gentle byte!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Person 1 — schema, migration, and sweep handler for document reminders' accurately and comprehensively summarizes the main changes: Prisma schema additions, a SQL migration, and the implementation of the sweep handler for document reminders.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/doc-2147-schema-job-infrastructure

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 25

🤖 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/internal/send-reminders-sweep.handler.ts`:
- Around line 86-95: The Promise.allSettled call inside io.runTask discards
results so failed jobs are invisible; change it to capture the results (e.g.,
const results = await Promise.allSettled(dueRecipients.map(...))) and then
iterate results alongside dueRecipients to detect entries with status ===
'rejected' and log the failure including recipientId and envelopeId and the
rejection reason using the module's logger (or processLogger) before finishing
the task; keep the existing jobs.triggerJob calls and io.runTask wrapper but add
this post-processing to surface failing triggers from Promise.allSettled.
- Around line 97-106: The owner-digest fan-out currently fires jobs via
jobs.triggerJob inside io.runTask without logging per-job failures; update the
Promise.allSettled handling for Array.from(dueEnvelopesByTeam.entries()) so that
after awaiting Promise.allSettled you iterate the results and call the logger
(processLogger or the existing logger in this module) for any result.status ===
'rejected', including the teamId, envelopeIds, the job name
'send.owner.reminder.digest.email', and the rejection reason/error; keep using
io.runTask and jobs.triggerJob but add this post-settled failure-logging step so
failed triggerJob calls are surfaced for debugging.

In `@packages/lib/translations/de/web.po`:
- Around line 21-26: Several German msgstr entries are still empty (e.g., the
entry with msgid " · {0} day{1} remaining"); populate each empty msgstr with the
correct German translation preserving placeholders (e.g., " · {0} Tag{1}
verbleibend" using plural logic where {1} supplies 's' when needed) and apply
the same fix to the other empty msgid entries referenced in the review; ensure
you only change the msgstr fields, keep the original msgid and placeholder order
intact, and run the project’s i18n lint/validation after updating.

In `@packages/lib/translations/en/web.po`:
- Around line 16-20: The translatable strings are using JS ternary pluralization
(e.g., in template-sender-reminder-digest.tsx where the Trans contains "·
{doc.daysRemaining} day{doc.daysRemaining === 1 ? '' : 's'} remaining"); replace
those inline ternaries with Lingui's plural() macro to produce ICU MessageFormat
(use plural(doc.daysRemaining, { one: "# day remaining", other: "# days
remaining" }) inside the Trans), apply the same change to all other templates
referenced by web.po (the entries around the shown msgid and the other affected
ranges), then regenerate web.po so the .po file contains proper ICU plurals
instead of concatenated suffix logic.

In `@packages/lib/translations/es/web.po`:
- Around line 62-64: Add Spanish translations for the empty msgstr entries
(e.g., the toast message for the envelope-delete-dialog component: msgid
"\"{title}\" has been successfully hidden") so Spanish users don't see English
text; update the corresponding msgstr to an appropriate Spanish string (for
example "«{title}» se ha ocultado correctamente") and apply the same fix to the
other listed message IDs (4000-4002, 10026-10028, 10042-10044, 10804-10806,
13519-13521) ensuring plural/placeholder formatting matches the msgid
placeholders and context used by components such as envelope-delete-dialog.tsx.
- Around line 21-25: The msgstr entries for the new reminder copy (e.g. msgid "
· {0} day{1} remaining" referenced by template-sender-reminder-digest.tsx) are
empty — update each corresponding msgstr in packages/lib/translations/es/web.po
with proper Spanish translations preserving placeholders ({0}, {1}) and
punctuation/spacing; search the file for the other untranslated reminder msgid
occurrences listed (lines around 297-300, 339-341, 453-455, 801-815, 5665-5667,
7382-7384, 8272-8321) and provide localized Spanish strings for each so Spanish
users see translated email copy, audit-log text, and settings labels.
- Around line 10911-10913: The Spanish translation for the helper text used in
apps/remix/app/components/dialogs/team-member-create-dialog.tsx is missing; open
packages/lib/translations/es/web.po, locate the 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." and set its msgstr to a proper Spanish translation such
as "Para añadir miembros a este equipo, primero deben ser invitados a la
organización. Solo los administradores y gerentes de la organización pueden
invitar a nuevos miembros; ponte en contacto con uno de ellos para que invite a
los miembros en tu nombre.", then save the file and run any localization
validation/build steps.

In `@packages/lib/translations/fr/web.po`:
- Around line 62-64: Add French translations for the currently-empty msgstr
entries for the new hide/duplicate UI strings (e.g., the msgid "\"{title}\" has
been successfully hidden" in fr/web.po) so the French UI doesn't fall back to
English; update the msgstr for that msgid and the other untranslated entries
referenced (IDs/ranges 4000-4002, 10026-10044, 10804-10806, 13519-13521) with
correct French text for both "hidden" and "duplicated" to match the English
meanings and preserve the "{title}" placeholder.
- Around line 21-25: Several reminder-related gettext entries in
packages/lib/translations/fr/web.po (e.g., the msgid " · {0} day{1} remaining"
from packages/email/template-components/template-sender-reminder-digest.tsx and
the other reminder msgid strings referenced in the review) have empty msgstr
values; populate each corresponding msgstr with correct French translations
preserving placeholders {0} and {1} and plural logic (e.g., translate " · {0}
day{1} remaining" to something like " · {0} jour{1} restant" with proper
pluralization), and repeat this for the other empty entries mentioned (lines
ranges 297-300, 339-341, 453-455, 801-815, 5665-5667, 7382-7384, 8272-8295,
8305-8321) so all reminder email, audit-log and settings labels are translated.
- Around line 10911-10913: The French translation for the new team-members
guidance is missing in the PO entry whose msgid matches the guidance string used
by team-member-create-dialog
(apps/remix/app/components/dialogs/team-member-create-dialog.tsx); open the PO
entry with that exact msgid and replace the empty msgstr with the correct French
translation of the full guidance text so French UI users see the localized
message instead of English.

In `@packages/lib/translations/it/web.po`:
- Around line 62-64: The Italian translation file
packages/lib/translations/it/web.po is missing translations for several new UI
strings (e.g. the msgid "\"{title}\" has been successfully hidden" from
apps/remix/app/components/dialogs/envelope-delete-dialog.tsx and other
hide/duplicate/member-management messages); update web.po by providing Italian
msgstr values for the listed msgids (including those in the ranges 4000-4002,
10026-10044, 10804-10913, 13519-13521) so users see localized toasts and dialog
copy — locate each untranslated msgid in web.po and add the correct Italian
translation into its msgstr.
- Around line 297-300: The Italian .po entries for the new reminder flow are
empty; populate the msgstr fields with proper Italian translations for the shown
msgid strings (e.g. "{0} recipients pending", and other reminder/digest/admin
strings) and for the other affected msgids listed (lines referenced: 339-341,
453-455, 5665-5667, 7382-7384, 801-815, 8272-8295, 8315-8321) so the locale no
longer falls back to English; preserve placeholders (e.g. {0}) and any
contextual notes (like "placeholder {0}: doc.pendingRecipientCount") and ensure
grammar/pluralization is correct in each msgstr.
- Around line 21-24: The current PO entry for msgid " · {0} day{1} remaining"
(used by template-sender-reminder-digest.tsx) relies on an English-only suffix
placeholder and prevents correct Italian pluralization; update the source string
to use ICU plural syntax (move plural logic into the TSX source, e.g. a single
count placeholder with an ICU plural pattern such as one/other forms) so
extraction emits a translatable plural-aware msgid, then remove the two-part
placeholder form from the .po (and any similar occurrences referenced around
lines 8305-8313).

In `@packages/lib/translations/ja/web.po`:
- Around line 10911-10913: The Japanese translation for the new helper text is
missing in packages/lib/translations/ja/web.po; open the entry referenced by
apps/remix/app/components/dialogs/team-member-create-dialog.tsx and replace the
empty msgstr with an appropriate Japanese translation of the English msgid,
preserving punctuation and special characters (— em dash) and ensuring proper
UTF-8 encoding and plural handling if needed so the ja locale shows the
localized string.
- Around line 21-25: Several new reminder/digest i18n entries (e.g., the msgid "
· {0} day{1} remaining" referenced from template-sender-reminder-digest.tsx and
other msgid keys in the diff) have empty msgstr values so Japanese falls back to
English; update each corresponding msgstr in the ja catalog with correct
Japanese translations preserving placeholders ({0}, {1}) and plural intent, and
repeat this for the other empty ranges called out (around the listed line
groups) to fully localize reminder emails, digest subjects, audit-log copy, and
the owner-setting label.
- Around line 62-64: The Japanese .po entries for the new hide/duplicate
messages are missing translations, so add appropriate Japanese strings to the
msgstr fields for each msgid (e.g. "\"{title}\" has been successfully hidden",
the duplicate/hide error and toast msgids referenced from
apps/remix/app/components/dialogs/envelope-delete-dialog.tsx and the other
occurrences listed: IDs 4000-4002, 10026-10028, 10042-10044, 10804-10806,
13519-13521). Locate the msgid lines in packages/lib/translations/ja/web.po and
supply accurate Japanese translations for the success and error messages (and
any placeholder usage like {title}) ensuring plural/format placeholders are
preserved.

In `@packages/lib/translations/ko/web.po`:
- Around line 21-25: The new Korean translations in translations/ko/web.po have
empty msgstr values for recently added reminder/hide/duplicate messages (e.g.,
msgid " · {0} day{1} remaining" and the other msgid entries referenced);
populate each corresponding msgstr with natural Korean translations while
preserving all placeholders exactly as-is ({0}, {1}, {count}, {teamName},
{documentName}, etc.), matching plurality/grammar semantics where needed, and
ensure no placeholders are modified or removed across the listed msgid entries.

In `@packages/lib/translations/nl/web.po`:
- Around line 21-25: Several Dutch translation entries (e.g. the msgid " · {0}
day{1} remaining" used by
packages/email/template-components/template-sender-reminder-digest.tsx) have
empty msgstr values; populate each msgstr with the correct Dutch text,
preserving placeholders ({0}, {1}) and plural logic (e.g. " · {0} dag{1}
resterend" or appropriate Dutch phrasing), and apply the same fix to the other
msgid groups listed (lines 62-64, 297-300, etc.) so nl users see Dutch copy
instead of English. Ensure grammar and punctuation match the original intent and
run any localization checks after updating.

In `@packages/lib/translations/pl/web.po`:
- Around line 21-26: Add Polish translations for the empty msgstr entries
(including the one for msgid " · {0} day{1} remaining") in
packages/lib/translations/pl/web.po, preserving all placeholders ({0}, {1}) and
any markup like <0/> exactly as in the msgid; update each corresponding msgstr
with the correct Polish text (e.g., a localized phrase that keeps the day
pluralization via {1}) for the entries referenced (including those originating
from template-sender-reminder-digest.tsx) and the other ranges noted so the
reminder/digest/audit messages are translated.
- Around line 62-65: Several UI strings in the Polish PO file are untranslated;
update the empty msgstr entries (e.g., msgid "\"{title}\" has been successfully
hidden") with correct Polish translations and do the same for the hide flow,
template duplicate flow, and the new team-member restriction message entries
referenced by the IDs/ranges 4000-4002, 10026-10028, 10042-10044, 10804-10806,
and 10913-10916; locate the corresponding msgid lines in
packages/lib/translations/pl/web.po and replace each empty msgstr with the
appropriate Polish text, keeping placeholders like "{title}" intact and ensuring
grammar and pluralization match the original intent.
- Around line 10800-10803: The Polish translation for the template delete error
is incorrect: update the msgstr for the msgid "This template could not be
deleted at this time. Please try again." (used by
apps/remix/app/components/dialogs/envelope-delete-dialog.tsx) to reference
"szablonu" instead of "dokumentu" so it reads appropriately for templates
(replace the current "Nie można usunąć dokumentu. Spróbuj ponownie." with a
template-specific Polish string).

In `@packages/lib/translations/pt-BR/web.po`:
- Around line 57-59: The Portuguese (pt-BR) translation catalog has empty msgstr
entries for several new UI strings (e.g. the msgid "\"{title}\" has been
successfully hidden" referenced by
apps/remix/app/components/dialogs/envelope-delete-dialog.tsx) causing fallbacks
to English; update the pt-BR .po file by providing accurate Portuguese
translations for all untranslated msgid entries in the mentioned ranges
(including the envelope-delete-dialog toast and the new hide/duplicate flows and
team-member guidance), ensuring placeholders like {title} are preserved exactly
and that pluralization/formatting matches the original msgids (check the msgid
strings in the ranges 3995-3997, 10021-10039, 10799-10801, 10906-10908,
13514-13516 and fill every corresponding msgstr).
- Around line 16-20: Several pt-BR .po entries for the new reminder flows are
missing translations (msgstrs are empty), e.g. the msgid " · {0} day{1}
remaining" referenced from template-sender-reminder-digest.tsx; update those
msgstrs with accurate Portuguese translations preserving placeholders and
pluralization (keep {0}/{1} tokens intact). Search the file for the other empty
reminder-related msgid blocks called out (ranges around the comment: 292-295,
334-336, 448-450, 5660-5662, 7377-7379, 796-810, 8267-8316) and fill each msgstr
with the correct pt-BR text (including translations for digest subjects/bodies,
"Owner reminder digest" setting label, and audit log messages) ensuring grammar
and plural forms match the original English intent.

In `@packages/lib/translations/zh/web.po`:
- Around line 21-25: The zh translations file has many newly added msgid entries
left with empty msgstr (e.g., the reminder string " · {0} day{1} remaining"
referenced from template-sender-reminder-digest.tsx), causing fallback to
English; update each empty msgstr with proper Chinese localizations using the
same placeholders (for example translate " · {0} day{1} remaining" to " · 剩余 {0}
天" and ensure {0}/{1} placeholders are preserved), and repeat this for the other
listed msgid entries (lines noted in the comment) so all newly introduced keys
have Chinese msgstr values.

In
`@packages/prisma/migrations/20260416021003_add_document_reminder_log/migration.sql`:
- Around line 2-3: The migration must enforce valid reminder intervals: update
the ALTER TABLE for DocumentMeta to add a CHECK constraint that ensures
reminderIntervalDays is > 0 when reminders are enabled (e.g., CHECK
(reminderEnabled = false OR reminderIntervalDays > 0)), and give the constraint
a descriptive name (for example DocumentMeta_reminder_interval_check); keep
reminderIntervalDays nullable so it can remain NULL when reminderEnabled is
false, but prevent 0/negative values when reminderEnabled is true.
🪄 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: dcc57666-ae18-4d24-82f6-e1d260ab6a54

📥 Commits

Reviewing files that changed from the base of the PR and between e21637c and 6644494.

📒 Files selected for processing (17)
  • apps/remix/app/components/general/admin-global-settings-section.tsx
  • packages/lib/jobs/definitions/internal/send-reminders-sweep.handler.ts
  • packages/lib/translations/de/web.po
  • packages/lib/translations/en/web.po
  • packages/lib/translations/es/web.po
  • packages/lib/translations/fr/web.po
  • packages/lib/translations/it/web.po
  • packages/lib/translations/ja/web.po
  • packages/lib/translations/ko/web.po
  • packages/lib/translations/nl/web.po
  • packages/lib/translations/pl/web.po
  • packages/lib/translations/pt-BR/web.po
  • packages/lib/translations/zh/web.po
  • packages/lib/types/document-email.ts
  • packages/lib/utils/document-audit-logs.ts
  • packages/lib/utils/document.ts
  • packages/prisma/migrations/20260416021003_add_document_reminder_log/migration.sql

Comment thread packages/lib/jobs/definitions/internal/send-reminders-sweep.handler.ts Outdated
Comment on lines +97 to +106
await io.runTask('fan-out-owner-digests', async () => {
await Promise.allSettled(
Array.from(dueEnvelopesByTeam.entries()).map(async ([teamId, envelopeIds]) => {
await jobs.triggerJob({
name: 'send.owner.reminder.digest.email',
payload: { teamId, envelopeIds: Array.from(envelopeIds) },
});
}),
);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Same issue: log failures for owner digest job triggers.

Apply the same pattern as suggested above to log any failed triggerJob calls for owner digests.

♻️ Proposed fix
   await io.runTask('fan-out-owner-digests', async () => {
-    await Promise.allSettled(
+    const results = await Promise.allSettled(
       Array.from(dueEnvelopesByTeam.entries()).map(async ([teamId, envelopeIds]) => {
         await jobs.triggerJob({
           name: 'send.owner.reminder.digest.email',
           payload: { teamId, envelopeIds: Array.from(envelopeIds) },
         });
       }),
     );
+
+    const failures = results.filter((r) => r.status === 'rejected');
+    if (failures.length > 0) {
+      io.logger.warn(`${failures.length} owner digest jobs failed to trigger`);
+    }
   });
🤖 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 97 - 106, The owner-digest fan-out currently fires jobs via
jobs.triggerJob inside io.runTask without logging per-job failures; update the
Promise.allSettled handling for Array.from(dueEnvelopesByTeam.entries()) so that
after awaiting Promise.allSettled you iterate the results and call the logger
(processLogger or the existing logger in this module) for any result.status ===
'rejected', including the teamId, envelopeIds, the job name
'send.owner.reminder.digest.email', and the rejection reason/error; keep using
io.runTask and jobs.triggerJob but add this post-settled failure-logging step so
failed triggerJob calls are surfaced for debugging.

Comment on lines +21 to +26
#. 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 ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Populate missing German translations before merge.

These newly added keys still have empty msgstr, so German users will receive English fallback in reminder emails and UI notifications.

🌐 Proposed translation patch
 msgid " · {0} day{1} remaining"
-msgstr ""
+msgstr " · {0} Tag{1} verbleibend"

 msgid "\"{title}\" has been successfully hidden"
-msgstr ""
+msgstr "\"{title}\" wurde erfolgreich ausgeblendet"

 msgid "{0} recipients pending"
-msgstr ""
+msgstr "{0} ausstehende Empfänger"

 msgid "{count} documents in {teamName} are awaiting signatures"
-msgstr ""
+msgstr "{count} Dokumente in {teamName} warten auf Unterschriften"

 msgid "{senderName} is waiting for your signature."
-msgstr ""
+msgstr "{senderName} wartet auf Ihre Unterschrift."

 msgid "1 document in {teamName} is awaiting a signature"
-msgstr ""
+msgstr "1 Dokument in {teamName} wartet auf eine Unterschrift"

 msgid "1 recipient pending"
-msgstr ""
+msgstr "1 ausstehender Empfänger"

 msgid "Document hidden"
-msgstr ""
+msgstr "Dokument ausgeblendet"

 msgid "Hi {ownerName}, here's a summary of documents that still need attention."
-msgstr ""
+msgstr "Hallo {ownerName}, hier ist eine Zusammenfassung der Dokumente, die noch Aufmerksamkeit benötigen."

 msgid "Owner reminder digest"
-msgstr ""
+msgstr "Erinnerungszusammenfassung für Eigentümer"

 msgid "Reminder sent to {0}"
-msgstr ""
+msgstr "Erinnerung an {0} gesendet"

 msgid "Reminder: {count} documents in {teamName} are awaiting signatures"
-msgstr ""
+msgstr "Erinnerung: {count} Dokumente in {teamName} warten auf Unterschriften"

 msgid "Reminder: 1 document in {teamName} is awaiting a signature"
-msgstr ""
+msgstr "Erinnerung: 1 Dokument in {teamName} wartet auf eine Unterschrift"

 msgid "Reminder: you have {daysRemaining} day{0} left to sign \"{documentName}\""
-msgstr ""
+msgstr "Erinnerung: Sie haben noch {daysRemaining} Tag{0}, um \"{documentName}\" zu unterzeichnen"

 msgid "Reminder: you have {daysRemaining} day{0} left to sign<0/>\"{documentName}\""
-msgstr ""
+msgstr "Erinnerung: Sie haben noch {daysRemaining} Tag{0}, um<0/>\"{documentName}\" zu unterzeichnen"

 msgid "Reminder: your signature is still needed on \"{documentName}\""
-msgstr ""
+msgstr "Erinnerung: Ihre Unterschrift wird für \"{documentName}\" weiterhin benötigt"

 msgid "Reminder: your signature is still needed on<0/>\"{documentName}\""
-msgstr ""
+msgstr "Erinnerung: Ihre Unterschrift wird für<0/>\"{documentName}\" weiterhin benötigt"

 msgid "Template Duplicated"
-msgstr ""
+msgstr "Vorlage dupliziert"

 msgid "Template hidden"
-msgstr ""
+msgstr "Vorlage ausgeblendet"

 msgid "This template could not be duplicated at this time. Please try again."
-msgstr ""
+msgstr "Diese Vorlage konnte derzeit nicht dupliziert werden. Bitte versuchen Sie es erneut."

 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 "Um Mitglieder zu diesem Team hinzuzufügen, müssen sie zuerst in die Organisation eingeladen werden. Nur Organisationsadministratoren und -manager können neue Mitglieder einladen — bitte kontaktieren Sie eine dieser Personen, damit sie Mitglieder in Ihrem Namen einladen."

 msgid "Your template has been successfully duplicated."
-msgstr ""
+msgstr "Ihre Vorlage wurde erfolgreich dupliziert."

Also applies to: 62-65, 297-301, 339-342, 453-456, 801-804, 813-816, 4000-4003, 5665-5668, 7382-7385, 8272-8278, 8289-8292, 8293-8296, 8305-8309, 8310-8314, 8315-8318, 8319-8322, 10026-10029, 10042-10045, 10804-10807, 10911-10914, 13519-13522

🤖 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 - 26, Several German
msgstr entries are still empty (e.g., the entry with msgid " · {0} day{1}
remaining"); populate each empty msgstr with the correct German translation
preserving placeholders (e.g., " · {0} Tag{1} verbleibend" using plural logic
where {1} supplies 's' when needed) and apply the same fix to the other empty
msgid entries referenced in the review; ensure you only change the msgstr
fields, keep the original msgid and placeholder order intact, and run the
project’s i18n lint/validation after updating.

Comment on lines +16 to +20
#. 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 " · {0} day{1} remaining"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does Lingui support ICU plural messages, and is concatenating plural suffixes like day{pluralSuffix} or splitting singular/plural copy into separate English-only branches recommended for translatable messages?

💡 Result:

Yes, Lingui fully supports ICU plural messages. Official documentation explicitly demonstrates ICU MessageFormat syntax for plurals, such as {numBooks, plural, one {# book} other {# books}}, which is extracted and translated while adding language-specific forms (e.g., Czech adds few and many). Macros like plural transform into ICU at build time. Concatenating plural suffixes like day{pluralSuffix} or splitting singular/plural into separate English-only branches is not recommended for translatable messages. Lingui best practices emphasize using full ICU plural syntax within a single message for proper handling across languages with varying plural rules (e.g., English: one/other; Czech: one/few/many/other; Arabic: up to 6 forms). This keeps the entire message context for translators. Official guides show complete sentences in plural forms, like plural(numBooks, { one: "# book", other: "# books" }), and ESLint rules prevent nested translation functions that could lead to fragmented messages. Source code should use source locale's plural categories; translators adapt via ICU.

Citations:


🏁 Script executed:

# Find the referenced template file
fd -t f "template-sender-reminder-digest.tsx"

Repository: b-mackenzie-alexander/documenso

Length of output: 145


🏁 Script executed:

# Search for plural handling patterns in TSX/TS files related to "day" and "remaining"
rg "day.*remaining" --type ts --type tsx -B 2 -A 2

Repository: b-mackenzie-alexander/documenso

Length of output: 102


🏁 Script executed:

# Check how the web.po file was generated - look for build scripts or extraction configs
fd -t f "lingui.config\|extraction\|messages" | head -20

Repository: b-mackenzie-alexander/documenso

Length of output: 57


🏁 Script executed:

# Look at the specific line references to understand the context better
rg "day\{1\}" packages/lib/translations/en/web.po -B 3 -A 3

Repository: b-mackenzie-alexander/documenso

Length of output: 455


🏁 Script executed:

cat -n packages/email/template-components/template-sender-reminder-digest.tsx | head -100

Repository: b-mackenzie-alexander/documenso

Length of output: 3510


🏁 Script executed:

# Search for where the "day remaining" message is constructed in TSX files
rg "day.*remaining|daysRemaining" --type-list | head

Repository: b-mackenzie-alexander/documenso

Length of output: 464


🏁 Script executed:

# Try broader search without tsx type
rg "day.*remaining|daysRemaining" -B 5 -A 5 | head -50

Repository: b-mackenzie-alexander/documenso

Length of output: 3559


Use ICU plurals for the new reminder copy.

Lines 16-20, 292-295, 334-336, 796-810, and 8284-8308 contain English plural logic embedded in translatable messages. For example, line 66 in packages/email/template-components/template-sender-reminder-digest.tsx uses:

<Trans> · {doc.daysRemaining} day{doc.daysRemaining === 1 ? '' : 's'} remaining</Trans>

This concatenates plural suffixes via JavaScript ternary expressions, which breaks translation for locales with different plural rules (e.g., Czech has one/few/many/other forms; Arabic has up to six). Replace with Lingui's plural() macro to generate proper ICU MessageFormat syntax:

<Trans>
 · {plural(doc.daysRemaining, {
   one: "# day remaining",
   other: "# days remaining"
 })}
</Trans>

After refactoring all affected source templates, regenerate web.po.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/en/web.po` around lines 16 - 20, The translatable
strings are using JS ternary pluralization (e.g., in
template-sender-reminder-digest.tsx where the Trans contains "·
{doc.daysRemaining} day{doc.daysRemaining === 1 ? '' : 's'} remaining"); replace
those inline ternaries with Lingui's plural() macro to produce ICU MessageFormat
(use plural(doc.daysRemaining, { one: "# day remaining", other: "# days
remaining" }) inside the Trans), apply the same change to all other templates
referenced by web.po (the entries around the shown msgid and the other affected
ranges), then regenerate web.po so the .po file contains proper ICU plurals
instead of concatenated suffix logic.

Comment on lines +21 to +25
#. 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 ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Translate the new reminder copy before merge.

These msgstr values are still empty, so Spanish users will get English reminder emails, audit-log text, and the new settings label.

💬 Suggested translations
 msgid " · {0} day{1} remaining"
-msgstr ""
+msgstr " · {0} día{1} restante{1}"

 msgid "{0} recipients pending"
-msgstr ""
+msgstr "{0} destinatarios pendientes"

 msgid "{count} documents in {teamName} are awaiting signatures"
-msgstr ""
+msgstr "{count} documentos de {teamName} están pendientes de firma"

 msgid "{senderName} is waiting for your signature."
-msgstr ""
+msgstr "A {senderName} le falta tu firma."

 msgid "1 document in {teamName} is awaiting a signature"
-msgstr ""
+msgstr "1 documento de {teamName} está pendiente de firma"

 msgid "1 recipient pending"
-msgstr ""
+msgstr "1 destinatario pendiente"

 msgid "Hi {ownerName}, here's a summary of documents that still need attention."
-msgstr ""
+msgstr "Hola, {ownerName}: aquí tienes un resumen de los documentos que aún requieren atención."

 msgid "Owner reminder digest"
-msgstr ""
+msgstr "Resumen de recordatorios del propietario"

 msgid "Reminder sent to {0}"
-msgstr ""
+msgstr "Recordatorio enviado a {0}"

 msgid "Reminder: {count} documents in {teamName} are awaiting signatures"
-msgstr ""
+msgstr "Recordatorio: {count} documentos de {teamName} están pendientes de firma"

 msgid "Reminder: 1 document in {teamName} is awaiting a signature"
-msgstr ""
+msgstr "Recordatorio: 1 documento de {teamName} está pendiente de firma"

 msgid "Reminder: you have {daysRemaining} day{0} left to sign \"{documentName}\""
-msgstr ""
+msgstr "Recordatorio: {daysRemaining} día{0} restante{0} para firmar \"{documentName}\""

 msgid "Reminder: you have {daysRemaining} day{0} left to sign<0/>\"{documentName}\""
-msgstr ""
+msgstr "Recordatorio: {daysRemaining} día{0} restante{0} para firmar<0/>\"{documentName}\""

 msgid "Reminder: your signature is still needed on \"{documentName}\""
-msgstr ""
+msgstr "Recordatorio: aún falta tu firma en \"{documentName}\""

 msgid "Reminder: your signature is still needed on<0/>\"{documentName}\""
-msgstr ""
+msgstr "Recordatorio: aún falta tu firma en<0/>\"{documentName}\""

Also applies to: 297-300, 339-341, 453-455, 801-815, 5665-5667, 7382-7384, 8272-8321

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/es/web.po` around lines 21 - 25, The msgstr entries
for the new reminder copy (e.g. msgid " · {0} day{1} remaining" referenced by
template-sender-reminder-digest.tsx) are empty — update each corresponding
msgstr in packages/lib/translations/es/web.po with proper Spanish translations
preserving placeholders ({0}, {1}) and punctuation/spacing; search the file for
the other untranslated reminder msgid occurrences listed (lines around 297-300,
339-341, 453-455, 801-815, 5665-5667, 7382-7384, 8272-8321) and provide
localized Spanish strings for each so Spanish users see translated email copy,
audit-log text, and settings labels.

Comment on lines +10800 to 10803
#: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx
msgid "This template could not be deleted at this time. Please try again."
msgstr "Nie można usunąć dokumentu. Spróbuj ponownie."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Template delete error still says “document”.

The msgid is template-specific, but the Polish copy still says dokumentu, which is misleading in the template delete flow.

💬 Suggested fix
 msgid "This template could not be deleted at this time. Please try again."
-msgstr "Nie można usunąć dokumentu. Spróbuj ponownie."
+msgstr "Nie można usunąć szablonu. Spróbuj ponownie."
📝 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.

Suggested change
#: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx
msgid "This template could not be deleted at this time. Please try again."
msgstr "Nie można usunąć dokumentu. Spróbuj ponownie."
#: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx
msgid "This template could not be deleted at this time. Please try again."
msgstr "Nie można usunąć szablonu. Spróbuj ponownie."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/pl/web.po` around lines 10800 - 10803, The Polish
translation for the template delete error is incorrect: update the msgstr for
the msgid "This template could not be deleted at this time. Please try again."
(used by apps/remix/app/components/dialogs/envelope-delete-dialog.tsx) to
reference "szablonu" instead of "dokumentu" so it reads appropriately for
templates (replace the current "Nie można usunąć dokumentu. Spróbuj ponownie."
with a template-specific Polish string).

Comment on lines +16 to +20
#. 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 ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Translate the new reminder flows before merging.

All new pt-BR entries for the reminder feature still have empty msgstrs. That means reminder emails, digest subjects/bodies, the Owner reminder digest setting, and the new reminder audit log will ship in English for Brazilian users.

Also applies to: 292-295, 334-336, 448-450, 5660-5662, 7377-7379, 796-810, 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, Several pt-BR
.po entries for the new reminder flows are missing translations (msgstrs are
empty), e.g. the msgid " · {0} day{1} remaining" referenced from
template-sender-reminder-digest.tsx; update those msgstrs with accurate
Portuguese translations preserving placeholders and pluralization (keep {0}/{1}
tokens intact). Search the file for the other empty reminder-related msgid
blocks called out (ranges around the comment: 292-295, 334-336, 448-450,
5660-5662, 7377-7379, 796-810, 8267-8316) and fill each msgstr with the correct
pt-BR text (including translations for digest subjects/bodies, "Owner reminder
digest" setting label, and audit log messages) ensuring grammar and plural forms
match the original English intent.

Comment on lines +57 to +59
#: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx
msgid "\"{title}\" has been successfully hidden"
msgstr ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fill the remaining new pt-BR UI strings as well.

These added toasts/dialog messages are also untranslated, so hide/duplicate flows and the new team-member guidance will fall back to English in the Portuguese catalog.

Also applies to: 3995-3997, 10021-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, The Portuguese
(pt-BR) translation catalog has empty msgstr entries for several new UI strings
(e.g. the msgid "\"{title}\" has been successfully hidden" referenced by
apps/remix/app/components/dialogs/envelope-delete-dialog.tsx) causing fallbacks
to English; update the pt-BR .po file by providing accurate Portuguese
translations for all untranslated msgid entries in the mentioned ranges
(including the envelope-delete-dialog toast and the new hide/duplicate flows and
team-member guidance), ensuring placeholders like {title} are preserved exactly
and that pluralization/formatting matches the original msgids (check the msgid
strings in the ranges 3995-3997, 10021-10039, 10799-10801, 10906-10908,
13514-13516 and fill every corresponding msgstr).

Comment on lines +21 to +25
#. 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 ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Populate newly added zh translations before release.

These changed entries are left with empty msgstr, so Chinese users will see English/fallback strings in reminder emails and related UI flows (hide/template duplicate/member invite text). Please provide localized values for all newly introduced keys in this PR.

If helpful, I can generate a complete proposed msgstr patch for these new keys.

Also applies to: 62-64, 297-300, 339-341, 453-455, 801-803, 813-815, 5665-5667, 7382-7384, 8272-8277, 8289-8295, 8305-8321, 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 21 - 25, The zh
translations file has many newly added msgid entries left with empty msgstr
(e.g., the reminder string " · {0} day{1} remaining" referenced from
template-sender-reminder-digest.tsx), causing fallback to English; update each
empty msgstr with proper Chinese localizations using the same placeholders (for
example translate " · {0} day{1} remaining" to " · 剩余 {0} 天" and ensure {0}/{1}
placeholders are preserved), and repeat this for the other listed msgid entries
(lines noted in the comment) so all newly introduced keys have Chinese msgstr
values.

Comment on lines +2 to +3
ALTER TABLE "DocumentMeta" ADD COLUMN "reminderEnabled" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "reminderIntervalDays" INTEGER;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a DB constraint for valid reminder intervals.

"reminderIntervalDays" currently accepts 0 and negative values. That can cause invalid/continuous reminder scheduling if app-level validation is bypassed. Add a CHECK constraint (e.g., > 0) and ideally couple it with reminderEnabled semantics.

Proposed migration adjustment
 ALTER TABLE "DocumentMeta" ADD COLUMN     "reminderEnabled" BOOLEAN NOT NULL DEFAULT false,
 ADD COLUMN     "reminderIntervalDays" INTEGER;
+
+ALTER TABLE "DocumentMeta"
+ADD CONSTRAINT "DocumentMeta_reminderIntervalDays_positive_check"
+CHECK ("reminderIntervalDays" IS NULL OR "reminderIntervalDays" > 0);
📝 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.

Suggested change
ALTER TABLE "DocumentMeta" ADD COLUMN "reminderEnabled" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "reminderIntervalDays" INTEGER;
ALTER TABLE "DocumentMeta" ADD COLUMN "reminderEnabled" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "reminderIntervalDays" INTEGER;
ALTER TABLE "DocumentMeta"
ADD CONSTRAINT "DocumentMeta_reminderIntervalDays_positive_check"
CHECK ("reminderIntervalDays" IS NULL OR "reminderIntervalDays" > 0);
🤖 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 2 - 3, The migration must enforce valid reminder intervals: update
the ALTER TABLE for DocumentMeta to add a CHECK constraint that ensures
reminderIntervalDays is > 0 when reminders are enabled (e.g., CHECK
(reminderEnabled = false OR reminderIntervalDays > 0)), and give the constraint
a descriptive name (for example DocumentMeta_reminder_interval_check); keep
reminderIntervalDays nullable so it can remain NULL when reminderEnabled is
false, but prevent 0/negative values when reminderEnabled is true.

Implement the cron sweep handler that queries pending envelopes with
reminders enabled, checks DocumentReminderLog for elapsed intervals,
and fans out per-recipient and per-team digest email jobs with
io.runTask idempotency wrappers.

Also includes minimal out-of-scope fixes required for npm run build:
- document-email.ts: add ownerReminderDigest to non-EMAIL fallback
- document-audit-logs.ts: add REMINDER_SENT exhaustive match case
- document.ts: add reminderEnabled/reminderIntervalDays to extractDerivedDocumentMeta
- admin-global-settings-section.tsx: add ownerReminderDigest label

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@b-mackenzie-alexander b-mackenzie-alexander force-pushed the feat/doc-2147-schema-job-infrastructure branch from 6644494 to 2b97b72 Compare April 18, 2026 18:55
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (19)
packages/lib/translations/pt-BR/web.po (2)

57-59: ⚠️ Potential issue | 🟡 Minor

Several new pt-BR UI strings remain empty.

New UI/dialog/toast strings are still untranslated (e.g., Line 57, Line 3995, Line 10021, Line 10037, Line 10799, Line 10906, Line 13514), which will also fall back to English. Please complete these msgstr entries.

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, The Portuguese
translation file has empty msgstr entries (e.g., the msgid "\"{title}\" has been
successfully hidden") causing fallback to English; for each empty entry
(including the other untranslated msgids called out in the review) add an
appropriate pt-BR translation, preserving placeholders like {title} and keeping
surrounding quotes/escaping intact, and ensure plural forms or context markers
(if present) are handled consistently with existing translations in the file.

16-20: ⚠️ Potential issue | 🟠 Major

Reminder-flow pt-BR entries are still untranslated.

These new reminder keys still have empty msgstr, so pt-BR users will receive English fallback in reminder emails/settings/audit copy (e.g., Line 16, Line 292, Line 334, Line 448, Line 796, Line 5660, Line 7377, Line 8267). Please fill all related msgstr values while preserving placeholders/plurals exactly.

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, Translate all
untranslated pt-BR entries for the reminder-flow in the .po file by filling each
empty msgstr while preserving the exact placeholders and plural markers (e.g.,
the entry with msgid " · {0} day{1} remaining" must keep "{0}" and "{1}"
unchanged and plural logic intact); update all referenced reminder msgid blocks
(including those at the other ranges noted) with correct Brazilian Portuguese
translations that maintain placeholders and plural forms exactly as in the
msgid.
packages/lib/translations/it/web.po (3)

21-25: ⚠️ Potential issue | 🟠 Major

Replace suffix-based pluralization with ICU plural forms

These msgids encode English grammar in placeholders (day{1} / day{0}), which prevents correct Italian pluralization even after translation.

Suggested source fix (in the TSX message, then re-extract catalogs)
- " · {0} day{1} remaining"
+ "{daysRemaining, plural, one { · # day remaining} other { · # days remaining}}"

Also applies to: 8305-8314

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/it/web.po` around lines 21 - 25, The msgid embeds
English suffix-based pluralization via placeholders (the template in
template-sender-reminder-digest.tsx using doc.daysRemaining and the {1} suffix)
which prevents correct Italian ICU pluralization; update the source message in
template-sender-reminder-digest.tsx to use an ICU plural form (e.g., a single
{count, plural, one {# day remaining} other {# days remaining}}-style
placeholder) driven by doc.daysRemaining, remove the two-part suffix
placeholders, then re-extract the PO catalogs so translations (including
packages/lib/translations/it/web.po and the other occurrences around lines
8305–8314) receive the new ICU msgid for proper plural handling.

297-301: ⚠️ Potential issue | 🟡 Minor

Populate untranslated reminder-flow strings

These new reminder/digest/audit entries still have empty msgstr, so Italian users will see English fallback in key reminder flows.

Also applies to: 339-342, 453-456, 5665-5668, 7382-7385, 801-804, 813-816, 8272-8278, 8289-8292, 8293-8296, 8315-8318, 8319-8322

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/it/web.po` around lines 297 - 301, The Italian .po
entries for reminder/digest/audit strings are missing translations (e.g., msgid
"{0} recipients pending" from
packages/email/template-components/template-sender-reminder-digest.tsx), causing
fallback to English; update the corresponding msgstr values with proper Italian
translations for this msgid and the other untranslated msgids noted (lines
referenced in the comment ranges) ensuring placeholders like {0} are preserved
exactly and context matches the template names (e.g.,
template-sender-reminder-digest.tsx) so the reminder flow displays Italian text.

62-65: ⚠️ Potential issue | 🟡 Minor

Add Italian translations for new hide/duplicate/member-management copy

These entries are still untranslated (msgstr ""), causing mixed-language UI and toasts in Italian.

Also applies to: 4000-4003, 10026-10029, 10042-10045, 10804-10807, 10911-10914, 13519-13522

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/it/web.po` around lines 62 - 65, The Italian
translation entries are empty causing mixed-language UI; for the msgid
"\"{title}\" has been successfully hidden" (from
apps/remix/app/components/dialogs/envelope-delete-dialog.tsx) and the other
untranslated msgid ranges (4000-4003, 10026-10029, 10042-10045, 10804-10807,
10911-10914, 13519-13522) fill each corresponding msgstr with the correct
Italian text while preserving the {title} placeholder and any escaping/quotes
exactly as in the msgid; update each msgstr in
packages/lib/translations/it/web.po (and identical entries in the listed ranges)
to the proper Italian phrasing so toasts and UI are fully localized.
packages/lib/translations/fr/web.po (3)

62-64: ⚠️ Potential issue | 🟡 Minor

The new hide/duplicate strings are still untranslated.

These empty msgstr values will surface English toasts and errors in the French UI for the new hide/duplicate flows.

Also applies to: 4000-4002, 10026-10044, 10804-10806, 13519-13521

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/fr/web.po` around lines 62 - 64, The French
translation file has empty msgstr entries for new hide/duplicate flows (e.g.,
the msgid "\"{title}\" has been successfully hidden" used by
apps/remix/app/components/dialogs/envelope-delete-dialog.tsx); populate the
corresponding msgstr with the correct French translations for that msgid and all
other untranslated msgids referred to (ranges 4000-4002, 10026-10044,
10804-10806, 13519-13521) so the UI shows French toasts/errors — locate the
entries by their msgid text and update each msgstr with the proper French
phrase, preserving placeholders like "{title}" and any surrounding quotes.

10911-10913: ⚠️ Potential issue | 🟡 Minor

This team-members guidance is still missing its French translation.

Leaving this msgstr empty will show the full guidance text in English in the French UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/fr/web.po` around lines 10911 - 10913, Add a French
translation for the PO entry whose msgid is "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." by filling the empty msgstr with an accurate, localized French sentence
preserving meaning and tone (including punctuation and the em dash); update that
msgstr in the same PO entry so the French UI shows the guidance in French.

21-25: ⚠️ Potential issue | 🟠 Major

Translate the new reminder copy before shipping.

These reminder-related entries still have empty msgstr values, so French users will get English reminder emails, audit-log text, and settings labels for the new feature.

Also applies to: 297-300, 339-341, 453-455, 801-815, 5665-5667, 7382-7384, 8272-8295, 8305-8321

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/fr/web.po` around lines 21 - 25, The new reminder
strings (e.g., the msgid " · {0} day{1} remaining" found alongside
packages/email/template-components/template-sender-reminder-digest.tsx) are
missing French translations — update the corresponding msgstr entries in
packages/lib/translations/fr/web.po to provide proper French translations
(including correct plural handling for the {1} placeholder and preserving
placeholders {0}/{1}), and repeat the same for the other empty reminder msgstr
blocks referenced in the comment (the ranges listed) so that reminder emails,
audit-log text, and settings labels are localized for French users.
packages/lib/translations/es/web.po (3)

62-64: ⚠️ Potential issue | 🟡 Minor

Hide/duplicate template-document Spanish copy remains untranslated.

These entries still have empty msgstr values (for example Line 64, Line 4002, Line 10028, Line 10044, Line 10806, Line 13521), so Spanish flows will leak English strings.

Also applies to: 4000-4002, 10026-10028, 10042-10044, 10804-10806, 13519-13521

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/es/web.po` around lines 62 - 64, Several msgid
entries in packages/lib/translations/es/web.po (for example the msgid
"\"{title}\" has been successfully hidden" and other hide/duplicate
template-document strings referenced around the reported ranges) have empty
msgstr values causing English fallbacks; locate each untranslated msgid (e.g.,
"\"{title}\" has been successfully hidden" and the duplicate template-document
related msgids noted) and populate their msgstr with the correct Spanish
translations, ensuring placeholders like {title} are preserved (e.g.,
"\"{title}\" se ha ocultado correctamente") and apply the same fix to all listed
occurrences (lines around 62, 4000-4002, 10026-10028, 10042-10044, 10804-10806,
13519-13521).

10911-10913: ⚠️ Potential issue | 🟡 Minor

Team-member guidance string is still untranslated in Spanish.

Line 10913 remains empty, so this helper text will fall back to English in the Spanish UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/es/web.po` around lines 10911 - 10913, The Spanish
translation for the team-member guidance string is missing: locate the msgid in
packages/lib/translations/es/web.po (the entry referencing
apps/remix/app/components/dialogs/team-member-create-dialog.tsx) and fill msgstr
with an accurate Spanish translation of "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." ensuring proper punctuation and accents consistent with project Spanish
style.

21-25: ⚠️ Potential issue | 🟡 Minor

Reminder-related Spanish translations are still missing.

msgstr is still empty across the new reminder digest/email/audit strings (for example Line 25, Line 300, Line 341, Line 455, Line 5667, Line 7384, Line 8277), so Spanish users will continue seeing English fallback content.

Also applies to: 297-300, 339-341, 453-455, 801-803, 813-815, 5665-5667, 7382-7384, 8272-8278, 8289-8291, 8293-8295, 8305-8309, 8310-8314, 8315-8318, 8319-8321

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/es/web.po` around lines 21 - 25, Several
reminder-related Spanish translations are missing: locate the PO entries whose
msgid values include strings like " · {0} day{1} remaining" and the other
reminder/email/audit msgid entries referenced (e.g., the msgid patterns around
the reminder digest template), and provide appropriate Spanish text in each
corresponding msgstr while preserving placeholders {0} and {1} and grammatical
pluralization; update all empty msgstrs noted in the comment (including the
multiple occurrences around the reminder
digest/template-sender-reminder-digest.tsx entries) so Spanish users see
translated content instead of English fallback.
packages/lib/translations/ja/web.po (3)

62-64: ⚠️ Potential issue | 🟡 Minor

Fill missing Japanese translations for hide/duplicate template-document copy.

Line 62, Line 4000, Line 10026, Line 10042, Line 10804, and Line 13519 still have empty msgstr, which leaves new hide/duplicate success/error UX partially untranslated in ja.

Also applies to: 4000-4002, 10026-10028, 10042-10044, 10804-10806, 13519-13521

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/ja/web.po` around lines 62 - 64, The Japanese
translations are missing for several UX messages in
packages/lib/translations/ja/web.po (e.g. the msgid "\"{title}\" has been
successfully hidden" referenced from
apps/remix/app/components/dialogs/envelope-delete-dialog.tsx); locate all
occurrences of the relevant msgid strings for hide/duplicate success/error
(occurring around the commented offsets: 62, ~4000, ~10026, ~10042, ~10804,
~13519) and populate each corresponding msgstr with the correct Japanese
translation so the hide/duplicate success and error messages are fully
translated across the file.

21-25: ⚠️ Potential issue | 🟠 Major

Backfill new reminder/digest/audit Japanese strings.

Line 21, Line 297, Line 339, Line 453, Line 801, Line 813, Line 5665, Line 7382, Line 8272, Line 8289, and Line 8305 introduce reminder-related keys with empty msgstr, so ja falls back to English for reminder emails, digest subjects, audit-log text, and the owner setting label.

Also applies to: 297-300, 339-341, 453-455, 801-803, 813-815, 5665-5667, 7382-7384, 8272-8277, 8289-8295, 8305-8321

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/ja/web.po` around lines 21 - 25, Several
reminder/digest/audit-related msgid entries in
packages/lib/translations/ja/web.po (e.g., " · {0} day{1} remaining" from
packages/email/template-components/template-sender-reminder-digest.tsx and the
other reminder/digest/audit msgid keys referenced in the review) have empty
msgstr values; update web.po by providing proper Japanese translations for each
of those msgid strings so the ja locale no longer falls back to English. Locate
each empty msgstr block in web.po for the reminder/digest/audit keys mentioned
in the review and replace the empty string with the correct Japanese text,
keeping placeholders ({0}, {1}) intact and matching plural logic where
applicable.

10911-10913: ⚠️ Potential issue | 🟡 Minor

Translate the new team-member helper sentence.

Line 10911 has an empty msgstr, so this newly added helper text renders in English for Japanese users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/ja/web.po` around lines 10911 - 10913, The msgid
string used by team-member-create-dialog.tsx is missing a Japanese translation
in packages/lib/translations/ja/web.po — update the empty msgstr for the 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." by replacing the empty value with
an appropriate Japanese translation (for example:
"このチームにメンバーを追加するには、まず組織に招待されている必要があります。組織の管理者またはマネージャーのみが新しいメンバーを招待できますので、代わりに招待してもらえるようその方に連絡してください。")
so the helper text displays in Japanese for users.
packages/lib/translations/pl/web.po (3)

21-26: ⚠️ Potential issue | 🟠 Major

Fill missing Polish translations for new reminder/digest strings.

These entries still have empty msgstr, so reminder emails/digests and related audit copy will remain untranslated in Polish. Please populate all of them while preserving placeholders and tags exactly ({...}, <0/>).

Also applies to: 297-301, 339-342, 453-456, 801-804, 813-816, 5665-5668, 7382-7385, 8272-8277, 8289-8296, 8305-8321

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/pl/web.po` around lines 21 - 26, Several msgstr
entries for reminder/digest strings (e.g., the msgid " · {0} day{1} remaining"
coming from
packages/email/template-components/template-sender-reminder-digest.tsx) are
empty; update each corresponding msgstr with proper Polish translations while
preserving all placeholders exactly (e.g., {0}, {1}) and any tags like <0/>; do
the same for the other similar reminder/digest msgid entries referenced in the
review so no reminder emails/audit copy remain untranslated.

10800-10803: ⚠️ Potential issue | 🟡 Minor

Template delete error uses “dokumentu” instead of “szablonu”.

This string is template-specific, so the current Polish text is misleading.

💡 Suggested fix
 msgid "This template could not be deleted at this time. Please try again."
-msgstr "Nie można usunąć dokumentu. Spróbuj ponownie."
+msgstr "Nie można usunąć szablonu. Spróbuj ponownie."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/pl/web.po` around lines 10800 - 10803, The Polish
translation for the template deletion error uses "dokumentu" instead of the
correct domain word "szablonu": update the msgstr for msgid "This template could
not be deleted at this time. Please try again." in
packages/lib/translations/pl/web.po (used by the envelope delete UI in
apps/remix/app/components/dialogs/envelope-delete-dialog.tsx /
EnvelopeDeleteDialog) to replace "Nie można usunąć dokumentu. Spróbuj ponownie."
with a variant that uses "szablonu" (e.g., "Nie można usunąć szablonu. Spróbuj
ponownie."), preserving punctuation and grammatical form.

62-65: ⚠️ Potential issue | 🟠 Major

New UI strings are still untranslated (msgstr "").

These will surface as English/fallback text in Polish flows (hide/duplicate/template/team-member restriction). Please provide Polish msgstr for each key.

Also applies to: 4000-4003, 10026-10028, 10042-10044, 10804-10806, 10913-10916, 13521-13524

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/pl/web.po` around lines 62 - 65, Translate the
untranslated English UI strings by replacing each empty msgstr with the correct
Polish translation; specifically update the msgstr for msgid "\"{title}\" has
been successfully hidden" (used by envelope-delete-dialog.tsx) and similarly
provide Polish translations for the other untranslated keys referenced
(IDs/ranges 4000-4003, 10026-10028, 10042-10044, 10804-10806, 10913-10916,
13521-13524), ensuring placeholders like "{title}" are preserved exactly in the
Polish text and that pluralization or context is reflected if needed.
packages/lib/translations/ko/web.po (1)

21-25: ⚠️ Potential issue | 🟡 Minor

Fill empty Korean translations before merge

These newly added strings still have empty msgstr, so Korean users will see fallback English in reminders, digest subjects, hide actions, and duplicate flows. Please populate them and keep placeholders/tokens ({0}, {count}, {teamName}, {documentName}, <0/>, etc.) unchanged.

Also applies to: 62-65, 297-301, 339-342, 453-456, 801-804, 813-816, 4000-4003, 5665-5668, 7382-7385, 8272-8278, 8289-8296, 8305-8314, 8315-8322, 10026-10029, 10042-10045, 10804-10807, 10911-10914, 13519-13522

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/ko/web.po` around lines 21 - 25, The PR added empty
Korean translations in packages/lib/translations/ko/web.po (for example the
msgid " · {0} day{1} remaining" from template-sender-reminder-digest.tsx) so
Korean users fall back to English; fill the msgstr for that msgid and all other
empty msgstr entries referenced (the other ranges listed) with correct Korean
translations while preserving all placeholders/tokens exactly ({0}, {1},
{count}, {teamName}, {documentName}, <0/>, etc.) and matching any
pluralization/spacing from the original msgid; update only the msgstr values in
web.po.
packages/prisma/migrations/20260416021003_add_document_reminder_log/migration.sql (1)

2-3: ⚠️ Potential issue | 🟠 Major

Add a DB-level CHECK for valid reminder intervals.

Line 2-3 still allows reminderIntervalDays <= 0, which can produce invalid reminder cadence if app validation is bypassed.

Suggested migration patch
 ALTER TABLE "DocumentMeta" ADD COLUMN     "reminderEnabled" BOOLEAN NOT NULL DEFAULT false,
 ADD COLUMN     "reminderIntervalDays" INTEGER;
+
+ALTER TABLE "DocumentMeta"
+ADD CONSTRAINT "DocumentMeta_reminder_interval_check"
+CHECK ("reminderEnabled" = false OR "reminderIntervalDays" > 0);
🤖 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 2 - 3, Add a DB-level CHECK constraint to ensure
reminderIntervalDays is either NULL or > 0 to prevent non-positive intervals
(reference table DocumentMeta and columns reminderEnabled and
reminderIntervalDays). Modify the migration to ALTER TABLE "DocumentMeta" ADD
CONSTRAINT (e.g., DocumentMeta_reminder_interval_positive) CHECK
(reminderIntervalDays IS NULL OR reminderIntervalDays > 0); ensuring the
constraint name and condition reference reminderIntervalDays so existing rows
with NULL are allowed but values <=0 are rejected. If any existing rows might
violate this, handle them (update or set NULL) in the migration before adding
the constraint.
🤖 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/internal/send-reminders-sweep.handler.ts`:
- Around line 10-114: The handler function run currently performs the DB query
(prisma.envelope.findMany), computation of dueRecipients/dueEnvelopesByTeam, and
fan-out triggers (jobs.triggerJob) without idempotency guards; wrap each logical
step in io.runTask('unique-key', async () => { ... }) using distinct keys (e.g.,
'fetch-envelopes', 'compute-due-recipients:<jobIdOrTimestamp>',
'trigger-recipient-reminders:<jobIdOrTimestamp>',
'trigger-owner-digests:<jobIdOrTimestamp>') so the prisma.envelope.findMany
call, the loop that builds dueRecipients/dueEnvelopesByTeam, and the
Promise.allSettled trigger blocks are each executed inside io.runTask to ensure
retries are idempotent and avoid duplicate emails.

In `@packages/lib/translations/en/web.po`:
- Around line 334-336: The current reminder templates hardcode signing language
but the sweep (send-reminders-sweep.handler) targets all recipient roles; fix by
either (A) narrowing the sweep to signer-only recipients in
send-reminders-sweep.handler (filter recipients by role === 'signer' or by
isSigner predicate) or (B) making the templates and stub handlers role-aware:
update send-recipient-reminder-email.handler and
send-owner-reminder-digest-email to compute recipientActionVerb and a role
description (reuse RECIPIENT_ROLES_DESCRIPTION and the logic from
send-signing-email.handler) and pass those values into the templates instead of
the hardcoded "signature"/"Sign Document" strings so
approvers/viewers/assistants receive correct copy.

In `@packages/lib/translations/nl/web.po`:
- Around line 4000-4002: The translation entry for msgid "Document hidden" (used
by envelope-delete-dialog.tsx) is missing a Dutch msgstr; update the nl/web.po
entry for msgid "Document hidden" by setting msgstr to the Dutch translation
(e.g., "Document verborgen") so Dutch users see the localized toast.

---

Duplicate comments:
In `@packages/lib/translations/es/web.po`:
- Around line 62-64: Several msgid entries in
packages/lib/translations/es/web.po (for example the msgid "\"{title}\" has been
successfully hidden" and other hide/duplicate template-document strings
referenced around the reported ranges) have empty msgstr values causing English
fallbacks; locate each untranslated msgid (e.g., "\"{title}\" has been
successfully hidden" and the duplicate template-document related msgids noted)
and populate their msgstr with the correct Spanish translations, ensuring
placeholders like {title} are preserved (e.g., "\"{title}\" se ha ocultado
correctamente") and apply the same fix to all listed occurrences (lines around
62, 4000-4002, 10026-10028, 10042-10044, 10804-10806, 13519-13521).
- Around line 10911-10913: The Spanish translation for the team-member guidance
string is missing: locate the msgid in packages/lib/translations/es/web.po (the
entry referencing
apps/remix/app/components/dialogs/team-member-create-dialog.tsx) and fill msgstr
with an accurate Spanish translation of "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." ensuring proper punctuation and accents consistent with project Spanish
style.
- Around line 21-25: Several reminder-related Spanish translations are missing:
locate the PO entries whose msgid values include strings like " · {0} day{1}
remaining" and the other reminder/email/audit msgid entries referenced (e.g.,
the msgid patterns around the reminder digest template), and provide appropriate
Spanish text in each corresponding msgstr while preserving placeholders {0} and
{1} and grammatical pluralization; update all empty msgstrs noted in the comment
(including the multiple occurrences around the reminder
digest/template-sender-reminder-digest.tsx entries) so Spanish users see
translated content instead of English fallback.

In `@packages/lib/translations/fr/web.po`:
- Around line 62-64: The French translation file has empty msgstr entries for
new hide/duplicate flows (e.g., the msgid "\"{title}\" has been successfully
hidden" used by apps/remix/app/components/dialogs/envelope-delete-dialog.tsx);
populate the corresponding msgstr with the correct French translations for that
msgid and all other untranslated msgids referred to (ranges 4000-4002,
10026-10044, 10804-10806, 13519-13521) so the UI shows French toasts/errors —
locate the entries by their msgid text and update each msgstr with the proper
French phrase, preserving placeholders like "{title}" and any surrounding
quotes.
- Around line 10911-10913: Add a French translation for the PO entry whose msgid
is "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." by filling the empty msgstr with
an accurate, localized French sentence preserving meaning and tone (including
punctuation and the em dash); update that msgstr in the same PO entry so the
French UI shows the guidance in French.
- Around line 21-25: The new reminder strings (e.g., the msgid " · {0} day{1}
remaining" found alongside
packages/email/template-components/template-sender-reminder-digest.tsx) are
missing French translations — update the corresponding msgstr entries in
packages/lib/translations/fr/web.po to provide proper French translations
(including correct plural handling for the {1} placeholder and preserving
placeholders {0}/{1}), and repeat the same for the other empty reminder msgstr
blocks referenced in the comment (the ranges listed) so that reminder emails,
audit-log text, and settings labels are localized for French users.

In `@packages/lib/translations/it/web.po`:
- Around line 21-25: The msgid embeds English suffix-based pluralization via
placeholders (the template in template-sender-reminder-digest.tsx using
doc.daysRemaining and the {1} suffix) which prevents correct Italian ICU
pluralization; update the source message in template-sender-reminder-digest.tsx
to use an ICU plural form (e.g., a single {count, plural, one {# day remaining}
other {# days remaining}}-style placeholder) driven by doc.daysRemaining, remove
the two-part suffix placeholders, then re-extract the PO catalogs so
translations (including packages/lib/translations/it/web.po and the other
occurrences around lines 8305–8314) receive the new ICU msgid for proper plural
handling.
- Around line 297-301: The Italian .po entries for reminder/digest/audit strings
are missing translations (e.g., msgid "{0} recipients pending" from
packages/email/template-components/template-sender-reminder-digest.tsx), causing
fallback to English; update the corresponding msgstr values with proper Italian
translations for this msgid and the other untranslated msgids noted (lines
referenced in the comment ranges) ensuring placeholders like {0} are preserved
exactly and context matches the template names (e.g.,
template-sender-reminder-digest.tsx) so the reminder flow displays Italian text.
- Around line 62-65: The Italian translation entries are empty causing
mixed-language UI; for the msgid "\"{title}\" has been successfully hidden"
(from apps/remix/app/components/dialogs/envelope-delete-dialog.tsx) and the
other untranslated msgid ranges (4000-4003, 10026-10029, 10042-10045,
10804-10807, 10911-10914, 13519-13522) fill each corresponding msgstr with the
correct Italian text while preserving the {title} placeholder and any
escaping/quotes exactly as in the msgid; update each msgstr in
packages/lib/translations/it/web.po (and identical entries in the listed ranges)
to the proper Italian phrasing so toasts and UI are fully localized.

In `@packages/lib/translations/ja/web.po`:
- Around line 62-64: The Japanese translations are missing for several UX
messages in packages/lib/translations/ja/web.po (e.g. the msgid "\"{title}\" has
been successfully hidden" referenced from
apps/remix/app/components/dialogs/envelope-delete-dialog.tsx); locate all
occurrences of the relevant msgid strings for hide/duplicate success/error
(occurring around the commented offsets: 62, ~4000, ~10026, ~10042, ~10804,
~13519) and populate each corresponding msgstr with the correct Japanese
translation so the hide/duplicate success and error messages are fully
translated across the file.
- Around line 21-25: Several reminder/digest/audit-related msgid entries in
packages/lib/translations/ja/web.po (e.g., " · {0} day{1} remaining" from
packages/email/template-components/template-sender-reminder-digest.tsx and the
other reminder/digest/audit msgid keys referenced in the review) have empty
msgstr values; update web.po by providing proper Japanese translations for each
of those msgid strings so the ja locale no longer falls back to English. Locate
each empty msgstr block in web.po for the reminder/digest/audit keys mentioned
in the review and replace the empty string with the correct Japanese text,
keeping placeholders ({0}, {1}) intact and matching plural logic where
applicable.
- Around line 10911-10913: The msgid string used by
team-member-create-dialog.tsx is missing a Japanese translation in
packages/lib/translations/ja/web.po — update the empty msgstr for the 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." by replacing the empty value with an
appropriate Japanese translation (for example:
"このチームにメンバーを追加するには、まず組織に招待されている必要があります。組織の管理者またはマネージャーのみが新しいメンバーを招待できますので、代わりに招待してもらえるようその方に連絡してください。")
so the helper text displays in Japanese for users.

In `@packages/lib/translations/ko/web.po`:
- Around line 21-25: The PR added empty Korean translations in
packages/lib/translations/ko/web.po (for example the msgid " · {0} day{1}
remaining" from template-sender-reminder-digest.tsx) so Korean users fall back
to English; fill the msgstr for that msgid and all other empty msgstr entries
referenced (the other ranges listed) with correct Korean translations while
preserving all placeholders/tokens exactly ({0}, {1}, {count}, {teamName},
{documentName}, <0/>, etc.) and matching any pluralization/spacing from the
original msgid; update only the msgstr values in web.po.

In `@packages/lib/translations/pl/web.po`:
- Around line 21-26: Several msgstr entries for reminder/digest strings (e.g.,
the msgid " · {0} day{1} remaining" coming from
packages/email/template-components/template-sender-reminder-digest.tsx) are
empty; update each corresponding msgstr with proper Polish translations while
preserving all placeholders exactly (e.g., {0}, {1}) and any tags like <0/>; do
the same for the other similar reminder/digest msgid entries referenced in the
review so no reminder emails/audit copy remain untranslated.
- Around line 10800-10803: The Polish translation for the template deletion
error uses "dokumentu" instead of the correct domain word "szablonu": update the
msgstr for msgid "This template could not be deleted at this time. Please try
again." in packages/lib/translations/pl/web.po (used by the envelope delete UI
in apps/remix/app/components/dialogs/envelope-delete-dialog.tsx /
EnvelopeDeleteDialog) to replace "Nie można usunąć dokumentu. Spróbuj ponownie."
with a variant that uses "szablonu" (e.g., "Nie można usunąć szablonu. Spróbuj
ponownie."), preserving punctuation and grammatical form.
- Around line 62-65: Translate the untranslated English UI strings by replacing
each empty msgstr with the correct Polish translation; specifically update the
msgstr for msgid "\"{title}\" has been successfully hidden" (used by
envelope-delete-dialog.tsx) and similarly provide Polish translations for the
other untranslated keys referenced (IDs/ranges 4000-4003, 10026-10028,
10042-10044, 10804-10806, 10913-10916, 13521-13524), ensuring placeholders like
"{title}" are preserved exactly in the Polish text and that pluralization or
context is reflected if needed.

In `@packages/lib/translations/pt-BR/web.po`:
- Around line 57-59: The Portuguese translation file has empty msgstr entries
(e.g., the msgid "\"{title}\" has been successfully hidden") causing fallback to
English; for each empty entry (including the other untranslated msgids called
out in the review) add an appropriate pt-BR translation, preserving placeholders
like {title} and keeping surrounding quotes/escaping intact, and ensure plural
forms or context markers (if present) are handled consistently with existing
translations in the file.
- Around line 16-20: Translate all untranslated pt-BR entries for the
reminder-flow in the .po file by filling each empty msgstr while preserving the
exact placeholders and plural markers (e.g., the entry with msgid " · {0} day{1}
remaining" must keep "{0}" and "{1}" unchanged and plural logic intact); update
all referenced reminder msgid blocks (including those at the other ranges noted)
with correct Brazilian Portuguese translations that maintain placeholders and
plural forms exactly as in the msgid.

In
`@packages/prisma/migrations/20260416021003_add_document_reminder_log/migration.sql`:
- Around line 2-3: Add a DB-level CHECK constraint to ensure
reminderIntervalDays is either NULL or > 0 to prevent non-positive intervals
(reference table DocumentMeta and columns reminderEnabled and
reminderIntervalDays). Modify the migration to ALTER TABLE "DocumentMeta" ADD
CONSTRAINT (e.g., DocumentMeta_reminder_interval_positive) CHECK
(reminderIntervalDays IS NULL OR reminderIntervalDays > 0); ensuring the
constraint name and condition reference reminderIntervalDays so existing rows
with NULL are allowed but values <=0 are rejected. If any existing rows might
violate this, handle them (update or set NULL) in the migration before adding
the constraint.
🪄 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: 0d32b913-5b38-4eda-8071-86f643403400

📥 Commits

Reviewing files that changed from the base of the PR and between 6644494 and 2b97b72.

📒 Files selected for processing (13)
  • packages/lib/jobs/definitions/internal/send-reminders-sweep.handler.ts
  • packages/lib/translations/de/web.po
  • packages/lib/translations/en/web.po
  • packages/lib/translations/es/web.po
  • packages/lib/translations/fr/web.po
  • packages/lib/translations/it/web.po
  • packages/lib/translations/ja/web.po
  • packages/lib/translations/ko/web.po
  • packages/lib/translations/nl/web.po
  • packages/lib/translations/pl/web.po
  • packages/lib/translations/pt-BR/web.po
  • packages/lib/translations/zh/web.po
  • packages/prisma/migrations/20260416021003_add_document_reminder_log/migration.sql

Comment on lines 10 to 114
export const run = async ({ io }: { payload: TSendRemindersSweepJobDefinition; io: JobRunIO }) => {
// TODO(Person 1): Implement sweep logic.
//
// Steps:
// 1. Query pending envelopes with reminderEnabled=true and a set reminderIntervalDays.
// Filter to envelopes where the document is still PENDING.
//
// 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' },
// },
// },
// take: 1000,
// });
//
// 2. For each envelope and each unsigned recipient, determine if a reminder is due.
// A reminder is due when:
// (now - lastReminderSentAt) >= reminderIntervalDays
// where lastReminderSentAt is the most recent DocumentReminderLog.createdAt for
// that recipient, or the envelope sentAt (createdAt) if no log exists yet.
//
// Use DateTime from 'luxon' for date arithmetic.
//
// 3. Collect recipients due for a reminder. Cap at 1000 total across all envelopes.
//
// 4. Fan out per-recipient email jobs:
// await Promise.allSettled(
// dueRecipients.map((r) =>
// jobs.triggerJob({
// name: 'send.recipient.reminder.email',
// payload: { recipientId: r.id, envelopeId: r.envelopeId },
// }),
// ),
// );
//
// 5. Group due envelopes by teamId. For each team with at least one due envelope,
// trigger the digest job:
// await Promise.allSettled(
// Object.entries(byTeam).map(([teamId, envIds]) =>
// jobs.triggerJob({
// name: 'send.owner.reminder.digest.email',
// payload: { teamId: Number(teamId), envelopeIds: envIds },
// }),
// ),
// );

// Reference implementations:
// expire-recipients-sweep.handler.ts — sweep query pattern + Promise.allSettled fan-out
// process-recipient-expired.handler.ts — io.runTask idempotency pattern

void DocumentStatus;
void SigningStatus;
void DateTime;
void prisma;
void jobs;

io.logger.info('send-reminders-sweep: not yet implemented');
const now = DateTime.now();

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,
});

if (envelopes.length === 0) {
io.logger.info('No envelopes with reminders enabled');
return;
}

const dueRecipients: Array<{ id: number; envelopeId: string }> = [];
const dueEnvelopesByTeam = new Map<number, Set<string>>();

for (const envelope of envelopes) {
const intervalDays = envelope.documentMeta?.reminderIntervalDays;

if (!intervalDays) {
continue;
}

for (const recipient of envelope.recipients) {
const lastLog = envelope.reminderLogs.find((log) => log.recipientId === recipient.id);
const lastSentAt = lastLog
? DateTime.fromJSDate(lastLog.createdAt)
: DateTime.fromJSDate(envelope.createdAt);

const daysSinceLast = now.diff(lastSentAt, 'days').days;

if (daysSinceLast >= intervalDays) {
dueRecipients.push({ id: recipient.id, envelopeId: envelope.id });

const teamEnvelopes = dueEnvelopesByTeam.get(envelope.teamId) ?? new Set<string>();
teamEnvelopes.add(envelope.id);
dueEnvelopesByTeam.set(envelope.teamId, teamEnvelopes);
}

// Cap matches the take: 1000 query limit — remainder picked up in next cron run.
if (dueRecipients.length >= 1000) {
break;
}
}

if (dueRecipients.length >= 1000) {
break;
}
}

if (dueRecipients.length === 0) {
io.logger.info('No recipients due for reminders');
return;
}

io.logger.info(`Found ${dueRecipients.length} recipients due for reminders`);

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(dueEnvelopesByTeam.entries()).map(async ([teamId, envelopeIds]) => {
await jobs.triggerJob({
name: 'send.owner.reminder.digest.email',
payload: { teamId, envelopeIds: Array.from(envelopeIds) },
});
}),
);

for (const result of digestResults) {
if (result.status === 'rejected') {
io.logger.error('Failed to trigger owner digest', { reason: result.reason });
}
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: All handler steps must be wrapped in io.runTask() for idempotency.

The handler performs multiple steps (database query, fan-out triggers) without io.runTask() wrappers. If the job crashes mid-execution and retries, it will re-query and re-trigger all reminder emails, causing duplicate notifications.

🔒 Proposed fix to wrap steps in io.runTask()
 export const run = async ({ io }: { payload: TSendRemindersSweepJobDefinition; io: JobRunIO }) => {
   const now = DateTime.now();
 
-  const envelopes = await prisma.envelope.findMany({
+  const envelopes = await io.runTask('fetch-pending-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,
     });
+  });

   // ... processing logic remains the same ...

-  const recipientResults = await Promise.allSettled(
+  const recipientResults = await io.runTask('fan-out-recipient-reminders', async () => {
+    return Promise.allSettled(
       dueRecipients.map(async (r) => {
         await jobs.triggerJob({
           name: 'send.recipient.reminder.email',
           payload: { recipientId: r.id, envelopeId: r.envelopeId },
         });
       }),
     );
+  });

   // ... failure logging ...

-  const digestResults = await Promise.allSettled(
+  const digestResults = await io.runTask('fan-out-owner-digests', async () => {
+    return Promise.allSettled(
       Array.from(dueEnvelopesByTeam.entries()).map(async ([teamId, envelopeIds]) => {
         await jobs.triggerJob({
           name: 'send.owner.reminder.digest.email',
           payload: { teamId, envelopeIds: Array.from(envelopeIds) },
         });
       }),
     );
+  });

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.

Suggested change
export const run = async ({ io }: { payload: TSendRemindersSweepJobDefinition; io: JobRunIO }) => {
// TODO(Person 1): Implement sweep logic.
//
// Steps:
// 1. Query pending envelopes with reminderEnabled=true and a set reminderIntervalDays.
// Filter to envelopes where the document is still PENDING.
//
// 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' },
// },
// },
// take: 1000,
// });
//
// 2. For each envelope and each unsigned recipient, determine if a reminder is due.
// A reminder is due when:
// (now - lastReminderSentAt) >= reminderIntervalDays
// where lastReminderSentAt is the most recent DocumentReminderLog.createdAt for
// that recipient, or the envelope sentAt (createdAt) if no log exists yet.
//
// Use DateTime from 'luxon' for date arithmetic.
//
// 3. Collect recipients due for a reminder. Cap at 1000 total across all envelopes.
//
// 4. Fan out per-recipient email jobs:
// await Promise.allSettled(
// dueRecipients.map((r) =>
// jobs.triggerJob({
// name: 'send.recipient.reminder.email',
// payload: { recipientId: r.id, envelopeId: r.envelopeId },
// }),
// ),
// );
//
// 5. Group due envelopes by teamId. For each team with at least one due envelope,
// trigger the digest job:
// await Promise.allSettled(
// Object.entries(byTeam).map(([teamId, envIds]) =>
// jobs.triggerJob({
// name: 'send.owner.reminder.digest.email',
// payload: { teamId: Number(teamId), envelopeIds: envIds },
// }),
// ),
// );
// Reference implementations:
// expire-recipients-sweep.handler.ts — sweep query pattern + Promise.allSettled fan-out
// process-recipient-expired.handler.ts — io.runTask idempotency pattern
void DocumentStatus;
void SigningStatus;
void DateTime;
void prisma;
void jobs;
io.logger.info('send-reminders-sweep: not yet implemented');
const now = DateTime.now();
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,
});
if (envelopes.length === 0) {
io.logger.info('No envelopes with reminders enabled');
return;
}
const dueRecipients: Array<{ id: number; envelopeId: string }> = [];
const dueEnvelopesByTeam = new Map<number, Set<string>>();
for (const envelope of envelopes) {
const intervalDays = envelope.documentMeta?.reminderIntervalDays;
if (!intervalDays) {
continue;
}
for (const recipient of envelope.recipients) {
const lastLog = envelope.reminderLogs.find((log) => log.recipientId === recipient.id);
const lastSentAt = lastLog
? DateTime.fromJSDate(lastLog.createdAt)
: DateTime.fromJSDate(envelope.createdAt);
const daysSinceLast = now.diff(lastSentAt, 'days').days;
if (daysSinceLast >= intervalDays) {
dueRecipients.push({ id: recipient.id, envelopeId: envelope.id });
const teamEnvelopes = dueEnvelopesByTeam.get(envelope.teamId) ?? new Set<string>();
teamEnvelopes.add(envelope.id);
dueEnvelopesByTeam.set(envelope.teamId, teamEnvelopes);
}
// Cap matches the take: 1000 query limit — remainder picked up in next cron run.
if (dueRecipients.length >= 1000) {
break;
}
}
if (dueRecipients.length >= 1000) {
break;
}
}
if (dueRecipients.length === 0) {
io.logger.info('No recipients due for reminders');
return;
}
io.logger.info(`Found ${dueRecipients.length} recipients due for reminders`);
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(dueEnvelopesByTeam.entries()).map(async ([teamId, envelopeIds]) => {
await jobs.triggerJob({
name: 'send.owner.reminder.digest.email',
payload: { teamId, envelopeIds: Array.from(envelopeIds) },
});
}),
);
for (const result of digestResults) {
if (result.status === 'rejected') {
io.logger.error('Failed to trigger owner digest', { reason: result.reason });
}
}
};
export const run = async ({ io }: { payload: TSendRemindersSweepJobDefinition; io: JobRunIO }) => {
const now = DateTime.now();
const envelopes = await io.runTask('fetch-pending-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,
});
});
if (envelopes.length === 0) {
io.logger.info('No envelopes with reminders enabled');
return;
}
const dueRecipients: Array<{ id: number; envelopeId: string }> = [];
const dueEnvelopesByTeam = new Map<number, Set<string>>();
for (const envelope of envelopes) {
const intervalDays = envelope.documentMeta?.reminderIntervalDays;
if (!intervalDays) {
continue;
}
for (const recipient of envelope.recipients) {
const lastLog = envelope.reminderLogs.find((log) => log.recipientId === recipient.id);
const lastSentAt = lastLog
? DateTime.fromJSDate(lastLog.createdAt)
: DateTime.fromJSDate(envelope.createdAt);
const daysSinceLast = now.diff(lastSentAt, 'days').days;
if (daysSinceLast >= intervalDays) {
dueRecipients.push({ id: recipient.id, envelopeId: envelope.id });
const teamEnvelopes = dueEnvelopesByTeam.get(envelope.teamId) ?? new Set<string>();
teamEnvelopes.add(envelope.id);
dueEnvelopesByTeam.set(envelope.teamId, teamEnvelopes);
}
// Cap matches the take: 1000 query limit — remainder picked up in next cron run.
if (dueRecipients.length >= 1000) {
break;
}
}
if (dueRecipients.length >= 1000) {
break;
}
}
if (dueRecipients.length === 0) {
io.logger.info('No recipients due for reminders');
return;
}
io.logger.info(`Found ${dueRecipients.length} recipients due for reminders`);
const recipientResults = await io.runTask('fan-out-recipient-reminders', async () => {
return 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 io.runTask('fan-out-owner-digests', async () => {
return Promise.allSettled(
Array.from(dueEnvelopesByTeam.entries()).map(async ([teamId, envelopeIds]) => {
await jobs.triggerJob({
name: 'send.owner.reminder.digest.email',
payload: { teamId, 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 10 - 114, The handler function run currently performs the DB query
(prisma.envelope.findMany), computation of dueRecipients/dueEnvelopesByTeam, and
fan-out triggers (jobs.triggerJob) without idempotency guards; wrap each logical
step in io.runTask('unique-key', async () => { ... }) using distinct keys (e.g.,
'fetch-envelopes', 'compute-due-recipients:<jobIdOrTimestamp>',
'trigger-recipient-reminders:<jobIdOrTimestamp>',
'trigger-owner-digests:<jobIdOrTimestamp>') so the prisma.envelope.findMany
call, the loop that builds dueRecipients/dueEnvelopesByTeam, and the
Promise.allSettled trigger blocks are each executed inside io.runTask to ensure
retries are idempotent and avoid duplicate emails.

Comment on lines +334 to +336
#: packages/email/template-components/template-sender-reminder-digest.tsx
msgid "{count} documents in {teamName} are awaiting signatures"
msgstr "{count} documents in {teamName} are awaiting signatures"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -t f "send-reminders-sweep.handler.ts|document-reminder.tsx|sender-reminder-digest.tsx|template-document-reminder.tsx|template-sender-reminder-digest.tsx"

echo
echo "== Sweep/job role filtering =="
rg -n -C 3 'send\.recipient\.reminder\.email|send\.owner\.reminder\.digest\.email|RecipientRole|SIGNER|APPROVER|VIEWER|ASSISTANT|recipientActionVerb' \
  packages/lib/jobs packages/email

echo
echo "== Reminder template copy =="
rg -n -C 2 'waiting for your signature|awaiting signatures|signature is still needed|Sign Document' \
  packages/email packages/lib

Repository: b-mackenzie-alexander/documenso

Length of output: 49025


🏁 Script executed:

cat -n packages/lib/jobs/definitions/internal/send-reminders-sweep.handler.ts

Repository: b-mackenzie-alexander/documenso

Length of output: 4270


🏁 Script executed:

cat -n packages/lib/jobs/definitions/emails/send-recipient-reminder-email.handler.ts

Repository: b-mackenzie-alexander/documenso

Length of output: 4313


🏁 Script executed:

cat -n packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.handler.ts

Repository: b-mackenzie-alexander/documenso

Length of output: 3660


Reminder sweeps send reminders to all recipient roles but templates hardcode signing-specific copy.

The sweep handler (line 23–28 of send-reminders-sweep.handler.ts) filters recipients only by signing status, not role. This includes signers, approvers, viewers, and assistants. However, both handler implementations are stubs (send-recipient-reminder-email.handler.ts and send-owner-reminder-digest-email.handler.ts), and the templates currently hardcode signing-specific language: "{count} documents in {teamName} are awaiting signatures," "your signature is still needed," and "Sign Document." When implemented, recipients in non-signing roles (approver, viewer, assistant) will receive emails claiming they need to sign.

Either filter the sweep to signer-only recipients, or make the reminder template copy role-aware (similar to the pattern in send-signing-email.handler.ts, which uses RECIPIENT_ROLES_DESCRIPTION and recipientActionVerb).

Also applies to: 448-450, 796-798, 8284-8290, 8310-8316, 9180-9182

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/en/web.po` around lines 334 - 336, The current
reminder templates hardcode signing language but the sweep
(send-reminders-sweep.handler) targets all recipient roles; fix by either (A)
narrowing the sweep to signer-only recipients in send-reminders-sweep.handler
(filter recipients by role === 'signer' or by isSigner predicate) or (B) making
the templates and stub handlers role-aware: update
send-recipient-reminder-email.handler and send-owner-reminder-digest-email to
compute recipientActionVerb and a role description (reuse
RECIPIENT_ROLES_DESCRIPTION and the logic from send-signing-email.handler) and
pass those values into the templates instead of the hardcoded "signature"/"Sign
Document" strings so approvers/viewers/assistants receive correct copy.

Comment on lines +4000 to +4002
#: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx
msgid "Document hidden"
msgstr ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Translate the new “Document hidden” toast.

msgstr is still empty here, so Dutch users will get English after hiding a document. Fill this in before merge, e.g. Document verborgen.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/translations/nl/web.po` around lines 4000 - 4002, The
translation entry for msgid "Document hidden" (used by
envelope-delete-dialog.tsx) is missing a Dutch msgstr; update the nl/web.po
entry for msgid "Document hidden" by setting msgstr to the Dutch translation
(e.g., "Document verborgen") so Dutch users see the localized toast.

Changes the sweep fan-out from one digest per teamId to one digest per
(teamId, userId) pair. Each document creator now receives exactly one
digest scoped to their own pending documents, fixing the bug where only
firstEnvelope.user received a digest in multi-author teams.

Also adds userId to the digest job payload schema so the handler can
filter the findMany query to the correct owner's envelopes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/lib/jobs/definitions/internal/send-reminders-sweep.handler.ts (1)

13-35: ⚠️ Potential issue | 🔴 Critical

Wrap each handler step in io.runTask() to enforce idempotency on retries.

Line 13 (query) and Lines 93/108 (fan-out triggers) run outside io.runTask. A retry after partial progress can re-trigger reminder/digest jobs and send duplicates.

🔧 Minimal fix sketch
-  const envelopes = await prisma.envelope.findMany({
+  const envelopes = await io.runTask('fetch-pending-reminder-envelopes', async () => prisma.envelope.findMany({
     ...
-  });
+  }));

-  const recipientResults = await Promise.allSettled(
-    dueRecipients.map(async (r) => {
+  const recipientResults = await io.runTask('fan-out-recipient-reminders', async () => Promise.allSettled(
+    dueRecipients.map(async r => {
       await jobs.triggerJob({
         name: 'send.recipient.reminder.email',
         payload: { recipientId: r.id, envelopeId: r.envelopeId },
       });
     }),
-  );
+  ));

-  const digestResults = await Promise.allSettled(
-    Array.from(dueEnvelopesByOwner.values()).map(async ({ teamId, userId, envelopeIds }) => {
+  const digestResults = await io.runTask('fan-out-owner-digests', async () => 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) },
       });
     }),
-  );
+  ));
As per coding guidelines: `**/jobs/definitions/**/*.handler.ts`: “All job handler steps must be wrapped in \`io.runTask('unique-key', fn)\` for idempotency”.

Also applies to: 93-115

🤖 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 - 35, The envelope query and downstream fan-out must be made
idempotent by wrapping each step in io.runTask: wrap the
prisma.envelope.findMany call and the per-envelope fan-out triggers that enqueue
reminder/digest jobs (the code that iterates over envelopes, accesses
envelope.recipients/reminderLogs, and calls the enqueue/send functions) inside
io.runTask with a stable unique key (e.g.,
`send-reminders-sweep:batch:<timestamp>` for the query/batch and
`send-reminders-sweep:envelope:<envelope.id>` for per-envelope work) so retries
cannot re-run partial progress and duplicate jobs; ensure the function passed to
io.runTask contains the query or the per-envelope enqueue logic (not just
logging) and returns only after all fan-out operations are scheduled.
🤖 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/internal/send-reminders-sweep.handler.ts`:
- Around line 14-20: The sweep query's where clause must exclude soft-deleted
envelopes by adding a deletedAt: null condition; update the query (the where
object used in the send-reminders sweep handler) alongside existing predicates
(DocumentStatus.PENDING, documentMeta.reminderEnabled,
documentMeta.reminderIntervalDays) so that only records with deletedAt === null
are selected and reminders aren't sent for deleted envelopes.

---

Duplicate comments:
In `@packages/lib/jobs/definitions/internal/send-reminders-sweep.handler.ts`:
- Around line 13-35: The envelope query and downstream fan-out must be made
idempotent by wrapping each step in io.runTask: wrap the
prisma.envelope.findMany call and the per-envelope fan-out triggers that enqueue
reminder/digest jobs (the code that iterates over envelopes, accesses
envelope.recipients/reminderLogs, and calls the enqueue/send functions) inside
io.runTask with a stable unique key (e.g.,
`send-reminders-sweep:batch:<timestamp>` for the query/batch and
`send-reminders-sweep:envelope:<envelope.id>` for per-envelope work) so retries
cannot re-run partial progress and duplicate jobs; ensure the function passed to
io.runTask contains the query or the per-envelope enqueue logic (not just
logging) and returns only after all fan-out operations are scheduled.
🪄 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: 3957586e-9f7d-44f1-a029-d9789cb8a2bb

📥 Commits

Reviewing files that changed from the base of the PR and between 2b97b72 and 3580990.

📒 Files selected for processing (2)
  • packages/lib/jobs/definitions/emails/send-owner-reminder-digest-email.ts
  • packages/lib/jobs/definitions/internal/send-reminders-sweep.handler.ts

Comment on lines +14 to +20
where: {
status: DocumentStatus.PENDING,
documentMeta: {
reminderEnabled: true,
reminderIntervalDays: { not: null },
},
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Exclude soft-deleted envelopes in the sweep query.

The current where clause omits deletedAt: null, so reminders may be sent for deleted envelopes.

🔧 Suggested query patch
   const envelopes = await prisma.envelope.findMany({
     where: {
       status: DocumentStatus.PENDING,
+      deletedAt: null,
       documentMeta: {
         reminderEnabled: true,
         reminderIntervalDays: { not: null },
       },
     },
🤖 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 14 - 20, The sweep query's where clause must exclude soft-deleted
envelopes by adding a deletedAt: null condition; update the query (the where
object used in the send-reminders sweep handler) alongside existing predicates
(DocumentStatus.PENDING, documentMeta.reminderEnabled,
documentMeta.reminderIntervalDays) so that only records with deletedAt === null
are selected and reminders aren't sent for deleted envelopes.

Copy link
Copy Markdown
Owner

@b-mackenzie-alexander b-mackenzie-alexander left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code reviewed. Necessary fixes applied. Approved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants