Skip to content

✨ server: add debt repayment notification#722

Open
aguxez wants to merge 1 commit intofeature/webhook-queuefrom
debt-repay
Open

✨ server: add debt repayment notification#722
aguxez wants to merge 1 commit intofeature/webhook-queuefrom
debt-repay

Conversation

@aguxez
Copy link
Contributor

@aguxez aguxez commented Feb 5, 2026


This is part 2 of 2 in a stack made with GitButler:

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

changeset-bot bot commented Feb 5, 2026

🦋 Changeset detected

Latest commit: 1d662ab

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

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
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

CodeRabbit can use your project's `biome` configuration to improve the quality of JS/TS/CSS/JSON code reviews.

Add a configuration file to your project to customize how CodeRabbit runs biome.

@gemini-code-assist
Copy link

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

sentry bot commented Feb 5, 2026

✅ All tests passed.

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.

@aguxez aguxez force-pushed the feature/webhook-queue branch from 06d840d to 0c1eceb Compare February 23, 2026 19:32
@aguxez aguxez force-pushed the feature/webhook-queue branch from 0c1eceb to 82c77f5 Compare March 2, 2026 13:25
@aguxez aguxez force-pushed the feature/webhook-queue branch from 82c77f5 to 96db3ec Compare March 3, 2026 14:48
@aguxez aguxez closed this Mar 3, 2026
@aguxez aguxez reopened this Mar 3, 2026
@aguxez aguxez force-pushed the feature/webhook-queue branch from 96db3ec to 38b719e Compare March 3, 2026 17:28
@aguxez aguxez force-pushed the feature/webhook-queue branch from 38b719e to 616ebf5 Compare March 3, 2026 20:06
@aguxez aguxez force-pushed the feature/webhook-queue branch from 616ebf5 to bf7e914 Compare March 3, 2026 20:22
@aguxez aguxez force-pushed the feature/webhook-queue branch from bf7e914 to a5d75cc Compare March 4, 2026 13:44
@aguxez aguxez force-pushed the feature/webhook-queue branch from a5d75cc to 65ebdb9 Compare March 4, 2026 14:37
@aguxez aguxez force-pushed the feature/webhook-queue branch 4 times, most recently from 4bce409 to a5d5c22 Compare March 6, 2026 12:49
Copy link

@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: a9d65a5d97

ℹ️ 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".

{ maturity: nextMaturity, window: "24h" },
{
jobId: `check-debts-${nextMaturity}-24h`,
delay: Math.max(0, (nextMaturity - 24 * 3600 - now) * 1000),

Choose a reason for hiding this comment

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

P1 Badge Skip stale reminder windows when scheduling jobs

Using Math.max(0, ...) here enqueues reminder jobs immediately whenever the server starts after a reminder window has already passed (e.g., deploy/restart 2 hours before maturity still runs the "24h" job right away). Because server/index.ts calls scheduleMaturityChecks() on startup, this can send users inaccurate notifications ("due in 24 hours" / "due in 1 hour" when that is no longer true) and produce bursts of stale alerts after downtime; the scheduler should avoid creating jobs for windows that are already in the past.

Useful? React with 👍 / 👎.

sentry[bot]

This comment was marked as resolved.

Copy link

@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: a9ac13074c

ℹ️ 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 on lines +77 to +81
86_400,
"NX",
)
.then((r) => {
if (r === "OK") {

Choose a reason for hiding this comment

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

P1 Badge Set dedupe key only after push notification succeeds

Writing the notification:sent:* key before sendPushNotification means transient OneSignal failures permanently suppress that window’s alert: redis.set(..., "NX") succeeds, the push call rejects, and subsequent attempts (including retries or duplicate account rows) skip sending because the key already exists for 24h. In production this causes silent missed maturity notifications for affected users during temporary push-provider/API errors.

Useful? React with 👍 / 👎.

Copy link

@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 2 potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +130 to +158
async function checkDebts(chunk: { account: string }[], maturity: number): Promise<DebtCheckResult> {
const promises = chunk.map(({ account }) =>
publicClient.readContract({
address: previewerAddress,
abi: previewerAbi,
functionName: "exactly",
args: [account as `0x${string}`],
}),
);

const results = await Promise.allSettled(promises);
const accounts: DebtCheckResult["accounts"] = [];

for (const [index, result] of results.entries()) {
const entry = chunk[index];
if (!entry) continue;
const { account } = entry;
if (result.status === "rejected") {
captureException(result.reason, { extra: { account } });
continue;
}
const hasDebt = result.value.some((market) =>
market.fixedBorrowPositions.some((p) => p.maturity === BigInt(maturity) && p.position.principal > 0n),
);
accounts.push({ account, hasDebt });
}

return { accounts, contractCalls: chunk.length };
}

Choose a reason for hiding this comment

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

🚩 No worker rate limiter unlike the account queue worker

The maturity worker is created without a limiter option (server/utils/maturity.ts:160), unlike the account queue worker which has limiter: { max: 10, duration: 1000 } at server/utils/createCredential.ts:121. Inside checkDebts, all accounts in a chunk (up to 50) fire concurrent readContract calls via Promise.allSettled (server/utils/maturity.ts:140). This could cause bursts of 50 simultaneous RPC calls per chunk. Whether this is a concern depends on the RPC provider's rate limits. The allSettled pattern handles individual failures gracefully, so this isn't a correctness issue, but it may warrant a concurrency limit if the provider rate-limits read calls.

Open in Devin Review

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

Copy link

@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: 1d662abd9c

ℹ️ 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 on lines +106 to +108
if (window === "1h") {
try {
await scheduleMaturityChecks(maturity);

Choose a reason for hiding this comment

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

P1 Badge Keep scheduling when no 1h reminder job is enqueued

Rescheduling is gated behind processing a window: "1h" job, so a startup that happens in the last hour before maturity (or after queue state is wiped during that period) can enqueue no jobs at all and never schedule future maturities. In that scenario scheduleMaturityChecks() skips stale windows, no 1h job runs, and this if (window === "1h") branch is never reached again, leaving notifications disabled until a later manual restart.

Useful? React with 👍 / 👎.

Copy link

@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 2 new potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +193 to +215
const delay24h = (nextMaturity - 24 * 3600 - now) * 1000;
if (delay24h >= 0) {
await queue.add(
"check-debts",
{ maturity: nextMaturity, window: "24h" },
{
jobId: `check-debts-${nextMaturity}-24h`,
delay: delay24h,
},
);
}

const delay1h = (nextMaturity - 3600 - now) * 1000;
if (delay1h >= 0) {
await queue.add(
"check-debts",
{ maturity: nextMaturity, window: "1h" },
{
jobId: `check-debts-${nextMaturity}-1h`,
delay: delay1h,
},
);
}

Choose a reason for hiding this comment

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

🔴 Rescheduling chain permanently breaks when server starts within 1 hour of the next maturity

When scheduleMaturityChecks() is called on startup without arguments (server/index.ts:346), nextMaturity is computed as the next maturity after now. If the server starts within 1 hour of that maturity, both delay24h and delay1h are negative, so both if guards at lines 194 and 206 fail and no jobs are scheduled. Since rescheduling for the next maturity only happens inside the 1h window processor's finally block (server/utils/maturity.ts:106), and no 1h job was ever enqueued, no future maturity checks will ever be scheduled. The notification system silently and permanently stops until the next server restart.

Scenario walkthrough
  1. Server starts when now is between nextMaturity - 3600 and nextMaturity.
  2. delay24h = (nextMaturity - 86400 - now) * 1000 → negative → skipped.
  3. delay1h = (nextMaturity - 3600 - now) * 1000 → negative → skipped.
  4. No jobs enqueued → no 1h processor runs → scheduleMaturityChecks(maturity) in the finally block never fires → chain is broken forever.
Prompt for agents
In server/utils/maturity.ts, the scheduleMaturityChecks function (lines 186-216) needs to handle the case where both delay24h and delay1h are negative for the computed nextMaturity. When this happens, the function should advance to the following maturity (nextMaturity + MATURITY_INTERVAL) and attempt to schedule jobs for that one instead, to ensure the rescheduling chain is never broken. One approach: after the two if-blocks, check if neither job was scheduled, and if so, recursively call scheduleMaturityChecks(nextMaturity) or loop to try the next maturity. Make sure to avoid infinite recursion by bounding how far ahead it looks (e.g., one extra interval should suffice).
Open in Devin Review

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


import { sendPushNotification } from "./onesignal";
import publicClient from "./publicClient";
import { queue as redis } from "./redis";

Choose a reason for hiding this comment

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

🚩 BullMQ queue connection used for application-level Redis operations

The module imports { queue as redis } from ./redis (server/utils/redis.ts:8) — the connection configured with maxRetriesPerRequest: null for BullMQ. This same connection is then used for application-level get/set operations (notification dedup keys at lines 73, 81). While functionally correct, mixing BullMQ's dedicated connection with arbitrary commands is unusual. The default Redis export (without maxRetriesPerRequest: null) would be more appropriate for the dedup keys, keeping the BullMQ connection isolated for queue operations.

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.

1 participant