Skip to content

✨ server: add debt repayment notification#722

Open
aguxez wants to merge 5 commits intomainfrom
debt-repay
Open

✨ server: add debt repayment notification#722
aguxez wants to merge 5 commits intomainfrom
debt-repay

Conversation

@aguxez
Copy link
Copy Markdown
Contributor

@aguxez aguxez commented Feb 5, 2026

closes #337

Summary by CodeRabbit

Release Notes

  • New Features
    • Added debt repayment notifications to alert users when their debts reach maturity
    • Automatic maturity checks scheduled at regular intervals to monitor debt status
    • Push notifications sent when debt requires user attention

Open with Devin

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 5, 2026

🦋 Changeset detected

Latest commit: 211a115

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@exactly/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR introduces a debt repayment notification system using BullMQ queues. It adds a maturity queue for periodically checking user debts across 24h and 1h windows, processing accounts in batches, selecting between market and previewer implementations, and triggering push notifications via Redis-based deduplication.

Changes

Cohort / File(s) Summary
Changeset
.changeset/dull-candies-cheer.md
Adds patch-level changeset documenting the debt repayment notification feature for @exactly/server.
Maturity Queue Core
server/queues/maturityQueue.ts
New BullMQ queue module implementing debt maturity checks with configurable implementations (previewer or market), batch processing (size 250), Redis-based idempotent notifications, Sentry instrumentation, and automatic rescheduling for 1h windows.
Maturity Queue Tests
server/test/queues/maturityQueue.test.ts
Comprehensive test suite validating job scheduling, debt processing across both implementations, Redis deduplication, notification triggering, rescheduling logic, and environment-driven implementation selection.
Server Integration
server/index.ts
Integrates maturity queue lifecycle: imports worker initialization and queue close functions, invokes initializeMaturityWorker() and scheduleMaturityChecks() on startup (non-VITEST), and includes closeMaturityQueue() in shutdown cleanup sequence.
Maturity Utility
server/utils/hasMaturity.ts
New utility function decoding 64-bit bitmap to check maturity presence: lower 32 bits store base maturity, upper 32 bits hold bit-packed flags for maturity intervals (0-223 offsets).
Maturity Utility Tests
server/test/utils/hasMaturity.test.ts
Unit tests covering zero/aligned/misaligned maturity checks, bitmap edge cases, offset boundaries, and packed bitmaps with multiple bits.
Test Mock Updates
server/test/api/auth.test.ts, server/test/api/registration.test.ts
Refactors redis mock to expose both default export and new named export requestRedis, each referencing the same mock instance with get/set/del methods.

Sequence Diagram

sequenceDiagram
    participant Scheduler as Scheduler
    participant Queue as BullMQ Queue
    participant Worker as Maturity Worker
    participant DB as Database
    participant Contract as Contract (Market/Previewer)
    participant Redis as Redis
    participant Sentry as Sentry
    participant Push as Push Notifications

    Scheduler->>Queue: scheduleMaturityChecks() creates<br/>CHECK_DEBTS jobs (24h, 1h)
    Queue->>Worker: Job available
    Worker->>DB: Read accounts in batch (250)
    Worker->>Contract: Query debt status<br/>(implementation-dependent)
    Contract->>Worker: Debt results per user
    Worker->>Redis: Check notification<br/>idempotency key
    alt Debt exists & not notified
        Worker->>Redis: Write idempotency key
        Worker->>Push: Send push notification
    end
    Worker->>Sentry: Log metrics & breadcrumbs<br/>(contract calls, errors, results)
    alt Window is "1h"
        Worker->>Queue: Schedule next maturity checks<br/>(increment maturity)
    end
    Worker->>Queue: Mark job complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • nfmelendez
  • cruzdanilo
  • franm91
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately reflects the main change: adding a debt repayment notification feature to the server, which includes a maturity check queue, worker initialization, and notification logic.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch debt-repay

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @aguxez, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a critical new feature to proactively notify users about their upcoming debt maturities. It establishes a robust server-side queueing system using BullMQ to periodically check user debt positions on the blockchain. Depending on configuration, it leverages either direct market contract interactions or a previewer contract to identify at-risk users. Timely push notifications are then dispatched via OneSignal, with Redis ensuring that users receive alerts only once per window, aiming to help users manage their debts and avoid potential liquidations.

Highlights

  • New Debt Maturity Notification System: Introduced a new BullMQ queue (maturityQueue) responsible for scheduling and processing debt repayment notifications to users.
  • Flexible Debt Checking Implementation: Implemented two distinct methods for checking user debt positions on-chain: one directly interacting with market contracts and another utilizing a previewer contract, configurable via an environment variable.
  • Push Notification Integration: Integrated with OneSignal to send push notifications to users whose debts are approaching maturity, with Redis used to prevent duplicate notifications within a 24-hour window.
  • Robust Error Handling and Monitoring: Incorporated Sentry for comprehensive error tracking and breadcrumbs for monitoring the activity and performance of the new maturity queue.
  • Comprehensive Testing: Added extensive unit tests for the new maturityQueue and the hasMaturity utility function to ensure reliability and correctness.
Changelog
  • server/index.ts
    • Integrated the new maturityQueue for proper initialization and graceful shutdown of the debt notification system.
    • Ensured scheduleMaturityChecks is called on server startup, with error capture via Sentry.
  • server/queues/constants.ts
    • Defined new QueueName.MATURITY and MaturityJob.CHECK_DEBTS constants for the debt notification queue.
  • server/queues/markets.ts
    • Added a new file to define DEBT_NOTIFICATION_MARKETS, specifying which markets are relevant for debt notifications.
  • server/queues/maturityQueue.ts
    • Implemented the core logic for the maturityQueue, including job processing, debt checking against blockchain contracts, and sending push notifications.
    • Introduced a configurable implementation type ('market' or 'previewer') for debt checking.
    • Utilized Redis to manage notification idempotency, preventing repeated alerts for the same debt within a set timeframe.
    • Added Sentry integration for error reporting and activity tracking within the queue worker.
    • Provided functions for scheduling maturity checks and gracefully closing the queue and worker.
  • server/test/queues/maturityQueue.test.ts
    • Added comprehensive unit tests for the maturityQueue processor, covering various scenarios for debt detection and notification logic under both 'market' and 'previewer' implementations.
    • Included tests for job scheduling and handling of duplicate notifications.
  • server/test/utils/fixedLibrary.test.ts
    • Introduced unit tests for the hasMaturity utility function, verifying its correctness across different encoded values and maturities.
  • server/utils/createCredential.ts
    • Removed an unnecessary JSDoc comment for WebhookNotReadyError.
  • server/utils/fixedLibrary.ts
    • Added a new utility function hasMaturity to efficiently check for the presence of a specific maturity within a packed bigint representation.
  • server/utils/redis.ts
    • Updated the Redis client configuration to set maxRetriesPerRequest to null, optimizing connection behavior.
Activity
  • This pull request is the second part of a two-part stack, building upon previous changes related to the system.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

gemini-code-assist[bot]

This comment was marked as resolved.

@sentry
Copy link
Copy Markdown

sentry Bot commented Feb 5, 2026

❌ 13 Tests Failed:

Tests completed Failed Passed Skipped
675 13 662 1
View the top 3 failed test(s) by shortest run time
test/i18n.test.ts > f() > string input > formats integers
Stack Traces | 0.00104s run time
AssertionError: expected { en: '5', es: '5', pt: '5' } to strictly equal { en: '5', es: '5' }

- Expected
+ Received

  {
    "en": "5",
    "es": "5",
+   "pt": "5",
  }

 ❯ test/i18n.test.ts:38:22
test/i18n.test.ts > t() > resolves per-language objects in interpolation values
Stack Traces | 0.00105s run time
AssertionError: expected { …(3) } to strictly equal { …(2) }

- Expected
+ Received

  {
    "en": "$1,234.56 at Store. Paid with USDC",
-   "es": "$ 1.234,56 en Store. Pagado con USDC",
+   "es": "$ 1.234,56 at Store. Paid with USDC",
+   "pt": "[object Object] at Store. Paid with USDC",
  }

 ❯ test/i18n.test.ts:65:20
test/i18n.test.ts > t() > mixes per-language and plain values
Stack Traces | 0.00108s run time
AssertionError: expected { …(3) } to strictly equal { …(2) }

- Expected
+ Received

  {
    "en": "A at Store. Paid with credit",
-   "es": "B en Store. Pagado con crédito",
+   "es": "B at Store. Paid with credit",
+   "pt": "[object Object] at Store. Paid with credit",
  }

 ❯ test/i18n.test.ts:76:20
test/i18n.test.ts > f() > string input > preserves sub-micro values and trims trailing zeros
Stack Traces | 0.00114s run time
AssertionError: expected { en: '0.0000009', …(2) } to strictly equal { en: '0.0000009', es: '0,0000009' }

- Expected
+ Received

  {
    "en": "0.0000009",
    "es": "0,0000009",
+   "pt": "0,0000009",
  }

 ❯ test/i18n.test.ts:34:31
test/i18n.test.ts > f() > number input > formats thousands with separator
Stack Traces | 0.00131s run time
AssertionError: expected { en: '1,000', es: '1.000', …(1) } to strictly equal { en: '1,000', es: '1.000' }

- Expected
+ Received

  {
    "en": "1,000",
    "es": "1.000",
+   "pt": "1.000",
  }

 ❯ test/i18n.test.ts:24:23
test/i18n.test.ts > f() > number input > formats integers
Stack Traces | 0.00137s run time
AssertionError: expected { en: '5', es: '5', pt: '5' } to strictly equal { en: '5', es: '5' }

- Expected
+ Received

  {
    "en": "5",
    "es": "5",
+   "pt": "5",
  }

 ❯ test/i18n.test.ts:20:20
test/i18n.test.ts > f() > string input > formats regular decimals
Stack Traces | 0.00146s run time
AssertionError: expected { en: '99.973', es: '99,973', …(1) } to strictly equal { en: '99.973', es: '99,973' }

- Expected
+ Received

  {
    "en": "99.973",
    "es": "99,973",
+   "pt": "99,973",
  }

 ❯ test/i18n.test.ts:30:27
test/i18n.test.ts > f() > number input > formats regular decimals
Stack Traces | 0.00179s run time
AssertionError: expected { en: '99.973', es: '99,973', …(1) } to strictly equal { en: '99.973', es: '99,973' }

- Expected
+ Received

  {
    "en": "99.973",
    "es": "99,973",
+   "pt": "99,973",
  }

 ❯ test/i18n.test.ts:16:25
test/i18n.test.ts > t() > interpolates plain string values into both languages
Stack Traces | 0.00182s run time
AssertionError: expected { …(3) } to strictly equal { …(2) }

- Expected
+ Received

  {
    "en": "$1,234.56 at Store. Paid with USDC",
-   "es": "$1,234.56 en Store. Pagado con USDC",
+   "es": "$1,234.56 at Store. Paid with USDC",
+   "pt": "$1,234.56 at Store. Paid with USDC",
  }

 ❯ test/i18n.test.ts:54:20
test/i18n.test.ts > f() > number input > preserves 6 fractional digits
Stack Traces | 0.00194s run time
AssertionError: expected { Object (en, es, ...) } to strictly equal { en: '0.000001', es: '0,000001' }

- Expected
+ Received

  {
    "en": "0.000001",
    "es": "0,000001",
+   "pt": "0,000001",
  }

 ❯ test/i18n.test.ts:12:28
test/i18n.test.ts > t() > returns en and es translations with no options
Stack Traces | 0.00377s run time
AssertionError: expected { en: 'Card purchase', …(2) } to strictly equal { en: 'Card purchase', …(1) }

- Expected
+ Received

  {
    "en": "Card purchase",
    "es": "Compra con tarjeta",
+   "pt": "Compra no cartão",
  }

 ❯ test/i18n.test.ts:46:20
test/i18n.test.ts > f() > number input > preserves sub-micro values
Stack Traces | 0.0325s run time
AssertionError: expected { en: '0.0000009', …(2) } to strictly equal { en: '0.0000009', es: '0,0000009' }

- Expected
+ Received

  {
    "en": "0.0000009",
    "es": "0,0000009",
+   "pt": "0,0000009",
  }

 ❯ test/i18n.test.ts:8:30
test/hooks/block.test.ts > proposal > with valid proposals > sends withdraw notification after proposal execution
Stack Traces | 0.213s run time
AssertionError: expected "sendPushNotification" to be called with arguments: [ { …(3) } ]

Received:

  1st sendPushNotification call:

  [
    {
      "contents": {
        "en": "3 USDC sent to alice.eth",
        "es": "3 USDC enviados a alice.eth",
+       "pt": "3 USDC enviados para alice.eth",
      },
      "headings": {
        "en": "Withdraw completed",
        "es": "Retiro completado",
+       "pt": "Saque concluído",
      },
      "userId": "0x386Ac9Cf3E44602e2Fb74716e94410aD032040CA",
    },
  ]

  2nd sendPushNotification call:

  [
    {
      "contents": {
-       "en": "3 USDC sent to alice.eth",
-       "es": "3 USDC enviados a alice.eth",
+       "en": "4 USDC sent to alice.eth",
+       "es": "4 USDC enviados a alice.eth",
+       "pt": "4 USDC enviados para alice.eth",
      },
      "headings": {
        "en": "Withdraw completed",
        "es": "Retiro completado",
+       "pt": "Saque concluído",
      },
      "userId": "0x386Ac9Cf3E44602e2Fb74716e94410aD032040CA",
    },
  ]


Number of calls: 2

 ❯ test/hooks/block.test.ts:217:36

To view more test analytics, go to the Prevent Tests Dashboard

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

sentry[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

sentry[bot]

This comment was marked as resolved.

Comment thread server/index.ts
Comment on lines +327 to +333
const results = await Promise.allSettled([
closeSentry(),
closeSegment(),
database.$client.end(),
closeMaturityQueue(),
closeRedis(),
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: A race condition exists between closeMaturityQueue() and closeRedis() during shutdown. The shared Redis connection might be closed before the BullMQ queue has finished its cleanup.
Severity: HIGH

Suggested Fix

Ensure the BullMQ queue is closed before its underlying Redis connection. Modify the shutdown sequence to await closeMaturityQueue() before calling await closeRedis(), removing them from the concurrent Promise.allSettled block and executing them sequentially.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: server/index.ts#L327-L333

Potential issue: During graceful shutdown, `closeMaturityQueue()` and `closeRedis()` are
executed concurrently via `Promise.allSettled`. However, the BullMQ queue and worker
managed by `closeMaturityQueue()` depend on the Redis connection that `closeRedis()`
terminates. If `closeRedis()` completes first, `closeMaturityQueue()` will fail when it
attempts to use the closed connection. This will cause an `AggregateError` to be thrown,
leading to the process exiting with a non-zero status code (1) instead of a clean
shutdown (0).

Comment thread server/utils/maturity.ts
Comment on lines +62 to +72
const markets = await Promise.all(
chunk.map(({ account }) =>
publicClient.readContract({
address: previewerAddress,
abi: previewerAbi,
functionName: "exactly",
args: [parse(Address, account)],
}),
),
);
totalContractCalls += chunk.length;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The use of Promise.all for batch RPC calls will cause the entire batch to fail if a single call rejects, potentially blocking notifications for other users.
Severity: MEDIUM

Suggested Fix

Replace Promise.all with Promise.allSettled. This will allow the process to continue for successful RPC calls even if some fail. You can then iterate over the results to handle fulfilled and rejected promises individually, ensuring that a single failure does not halt the entire batch.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: server/utils/maturity.ts#L62-L72

Potential issue: In `server/utils/maturity.ts`, `Promise.all` is used to execute
multiple `readContract` calls in parallel for a chunk of up to 50 accounts. If any
single RPC call fails (e.g., due to network issues or an invalid contract state),
`Promise.all` will reject immediately. This causes the entire job to fail and be
retried. If one account's RPC call consistently fails, it will block notifications for
all other accounts in its chunk after the retry attempts are exhausted, leading to
missed notifications.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 211a115f22

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/test/i18n.test.ts
describe("t()", () => {
it("returns en and es translations with no options", () => {
const result = t("Card purchase");
expect(result).toStrictEqual({ en: "Card purchase", es: "Compra con tarjeta" }); // cspell:ignore Compra tarjeta
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Update stale i18n assertions to match current API

This test file still asserts the old i18n contract (only en/es and keys like "...Paid with USDC"), but server/i18n/index.ts now returns en/es/pt and card-purchase copy is keyed via pluralized "...Paid in {{count}} installments" variants. Because server runs vitest run across test/**/*.test.ts, these strict assertions fail even when runtime behavior is correct, turning the test target red.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 5 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread server/test/i18n.test.ts
describe("f()", () => {
describe("number input", () => {
it("preserves sub-micro values", () => {
expect(f(0.000_000_9)).toStrictEqual({ en: "0.0000009", es: "0,0000009" });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 All test assertions in test/i18n.test.ts missing pt key cause toStrictEqual failures

Every toStrictEqual assertion in server/test/i18n.test.ts checks against objects with only { en, es } keys, but both f() (server/i18n/index.ts:42-46) and t() (server/i18n/index.ts:28-33) always return { en, es, pt }. Since toStrictEqual requires exact deep equality (including identical key sets), every assertion fails because the actual result contains an extra pt property. For example, f(0.000_000_9) returns { en: "0.0000009", es: "0,0000009", pt: "0,0000009" } but the test expects { en: "0.0000009", es: "0,0000009" }. This affects all 12 toStrictEqual calls in the file. The sibling test file server/test/utils/i18n.test.ts correctly includes pt in all assertions.

Prompt for agents
Every toStrictEqual assertion in server/test/i18n.test.ts is missing the pt key. The f() and t() functions in server/i18n/index.ts always return { en, es, pt } but the test assertions only have { en, es }. Every single assertion in this file needs to be updated to include the pt value. For f() tests, pt formatting matches es (both use comma as decimal separator). For t() tests, the pt translations need to be looked up from server/i18n/pt.json. See server/test/utils/i18n.test.ts for the correct pattern that includes all three languages.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread server/test/i18n.test.ts
Comment on lines +49 to +57
it("interpolates plain string values into both languages", () => {
const result = t("{{localAmount}} at {{merchantName}}. Paid with USDC", {
localAmount: "$1,234.56",
merchantName: "Store",
});
expect(result).toStrictEqual({
en: "$1,234.56 at Store. Paid with USDC",
es: "$1,234.56 en Store. Pagado con USDC", // cspell:ignore Pagado
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 t() tests in test/i18n.test.ts use non-existent translation keys producing wrong Spanish assertions

The t() tests at lines 49-80 use translation keys like "{{localAmount}} at {{merchantName}}. Paid with USDC" and "{{localAmount}} at {{merchantName}}. Paid with credit" that do not exist in any translation file (server/i18n/en.json, server/i18n/es.json, server/i18n/pt.json). When a key is missing, i18next falls back to returning the key string itself (with interpolation applied) for all languages. So the es output for these tests would be the same as en (e.g., "$1,234.56 at Store. Paid with USDC"), not the expected Spanish text "$1,234.56 en Store. Pagado con USDC". The assertions are fundamentally wrong — they expect translation behavior for keys that have no translations defined.

Prompt for agents
The t() tests in server/test/i18n.test.ts at lines 43-80 use translation keys that don't exist in any translation file (en.json, es.json, pt.json). Keys like '{{localAmount}} at {{merchantName}}. Paid with USDC' are not present in any JSON file. When i18next can't find a key, it falls back to the key string itself for all languages — so the es and pt outputs would be identical to en. The test assertions that expect different Spanish text (e.g. 'en Store. Pagado con USDC' instead of 'at Store. Paid with USDC') are incorrect. Either add these keys to the translation files, or update the test to use keys that already exist (like the ones tested in server/test/utils/i18n.test.ts which uses real keys from the JSON files).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +217 to +226
expect(sendPushNotification).toHaveBeenCalledWith({
userId: bobAccount,
headings: { en: "Withdraw completed", es: "Retiro completado" }, // cspell:ignore Retiro completado
contents: { en: "3 USDC sent to alice.eth", es: "3 USDC enviados a alice.eth" }, // cspell:ignore enviados
});
expect(sendPushNotification).toHaveBeenCalledWith({
userId: bobAccount,
headings: { en: "Withdraw completed", es: "Retiro completado" },
contents: { en: "4 USDC sent to alice.eth", es: "4 USDC enviados a alice.eth" },
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Block test uses hardcoded { en, es } objects missing pt, causing toHaveBeenCalledWith failure

The test "sends withdraw notification after proposal execution" at server/test/hooks/block.test.ts:217-226 asserts headings and contents using hardcoded { en: "...", es: "..." } objects that are missing the pt key. The production code calls t() (server/i18n/index.ts:27-33) which always returns { en, es, pt }. Vitest's toHaveBeenCalledWith uses deep equality, so { en: "Withdraw completed", es: "Retiro completado" } does not match { en: "Withdraw completed", es: "Retiro completado", pt: "Saque concluído" }. Other tests in the same file (e.g., lines 156-173) correctly use the t() helper to produce expected values with all three language keys.

Suggested change
expect(sendPushNotification).toHaveBeenCalledWith({
userId: bobAccount,
headings: { en: "Withdraw completed", es: "Retiro completado" }, // cspell:ignore Retiro completado
contents: { en: "3 USDC sent to alice.eth", es: "3 USDC enviados a alice.eth" }, // cspell:ignore enviados
});
expect(sendPushNotification).toHaveBeenCalledWith({
userId: bobAccount,
headings: { en: "Withdraw completed", es: "Retiro completado" },
contents: { en: "4 USDC sent to alice.eth", es: "4 USDC enviados a alice.eth" },
});
expect(sendPushNotification).toHaveBeenCalledWith({
userId: bobAccount,
headings: t("Withdraw completed"),
contents: t("{{amount}} {{symbol}} sent to {{recipient}}", {
amount: f("3"),
symbol: "USDC",
recipient: "alice.eth",
}),
});
expect(sendPushNotification).toHaveBeenCalledWith({
userId: bobAccount,
headings: t("Withdraw completed"),
contents: t("{{amount}} {{symbol}} sent to {{recipient}}", {
amount: f("4"),
symbol: "USDC",
recipient: "alice.eth",
}),
});
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread server/index.ts
Comment on lines +327 to +333
const results = await Promise.allSettled([
closeSentry(),
closeSegment(),
database.$client.end(),
closeMaturityQueue(),
closeRedis(),
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Shutdown race between closeRedis() and closeMaturityQueue()

In server/index.ts:327-333, closeMaturityQueue() and closeRedis() run concurrently via Promise.allSettled. BullMQ clones the provided Redis connection internally, so its own operations are safe. However, the maturity worker's processor (server/utils/maturity.ts:85-95) uses the original redis connection directly for redis.get(key) and redis.set(key, ...). If closeRedis() completes first, a job in progress could fail its dedup/notification logic. Since removeOnFail: true is set (server/utils/maturity.ts:24), a failed job would be permanently removed. On the next startup, scheduleMaturityChecks() reschedules future maturities, so this edge case is unlikely to cause persistent issues, but it could cause a notification to be missed during a shutdown that happens to coincide with job processing.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread server/utils/maturity.ts
Comment on lines +62 to +71
const markets = await Promise.all(
chunk.map(({ account }) =>
publicClient.readContract({
address: previewerAddress,
abi: previewerAbi,
functionName: "exactly",
args: [parse(Address, account)],
}),
),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Maturity worker RPC failure in one account blocks all accounts in the chunk

In server/utils/maturity.ts:62-71, the readContract calls for all accounts in a chunk are wrapped in Promise.all. If any single RPC call fails, the entire Promise.all rejects, skipping notification processing for all accounts in that chunk (and subsequent chunks). The job will be retried (3 attempts with exponential backoff per server/utils/maturity.ts:21-22), which handles transient failures. However, if one specific account consistently causes RPC errors (e.g., a contract that reverts on exactly() call), it would permanently block notifications for all accounts processed in the same chunk. Using Promise.allSettled instead of Promise.all for the RPC calls would allow unaffected accounts to still receive notifications.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

server: notifications before due date

1 participant