Skip to content

Comments

feat: add job to check for expired hold invoices#746

Open
ndungudedan wants to merge 2 commits intolnp2pBot:mainfrom
ndungudedan:change-order-status
Open

feat: add job to check for expired hold invoices#746
ndungudedan wants to merge 2 commits intolnp2pBot:mainfrom
ndungudedan:change-order-status

Conversation

@ndungudedan
Copy link
Contributor

@ndungudedan ndungudedan commented Feb 15, 2026

  • Implemented a new job checkHoldInvoiceExpired to update orders with expired hold invoices.
  • Scheduled the job to run every minute in the bot's initialization.
  • Added a new status 'HOLD_INVOICE_EXPIRED' to the order model for tracking expired invoices.

Ressolves: #362

Summary by CodeRabbit

  • New Features

    • Automatic monitoring runs every minute to detect and mark orders whose hold invoices have expired; buyers and sellers receive expiry notifications.
  • Status

    • Added a new order status to track orders with expired hold invoices.
  • Localization

    • Added user-facing messages for hold-invoice expirations and pending payment outcomes in multiple languages.
  • Tests

    • Updated test expectations to account for the additional scheduled job.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 15, 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

Adds a scheduled job to detect expired LN hold invoices, cancel them, mark orders as HOLD_INVOICE_EXPIRED, notify buyer and seller, export the job, add message handlers, and add localization keys and test updates.

Changes

Cohort / File(s) Summary
Scheduler
bot/start.ts
Registers a new scheduled job to run checkHoldInvoiceExpired(bot) every minute (cron 0 * * * * *).
Job implementation & export
jobs/check_hold_invoice_expired.ts, jobs/index.ts
Adds checkHoldInvoiceExpired(bot: HasTelegram): Promise<void> — queries orders with expired invoice_held_at, acquires per-order mutex, attempts cancelHoldInvoice, updates status to HOLD_INVOICE_EXPIRED, persists, emits orderUpdated, and notifies buyer/seller; re-exported from jobs/index.ts.
Order model
models/order.ts
Adds new order status value HOLD_INVOICE_EXPIRED to the status enum.
Bot messaging
bot/messages.ts
Adds toBuyerHoldInvoiceExpiredMessage and toSellerHoldInvoiceExpiredMessage handlers and exports; send localized messages and log errors on failure.
Locales / translations
locales/*.yaml
Adds hold_invoice_expired_to_buyer and hold_invoice_expired_to_seller across multiple locale files; also adds several pending_payment_* and admin notification keys in some locales.
Tests
tests/bot/bot.spec.ts
Updates expected scheduleJob call counts from 7 → 8 to account for the new scheduled job.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • Catrya
  • grunch

Poem

🐇
I hop the minutes, check the gate,
When held invoices miss their date,
I cancel, mark, and softly tell,
To buyer, seller — all is well,
A rabbit's nudge: resolve and wait.

🚥 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 PR title accurately describes the main change: implementing a new job to check for expired hold invoices, which is the primary objective across all modified files.
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 unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
Contributor

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

🤖 Fix all issues with AI agents
In `@jobs/check_hold_invoice_expired.ts`:
- Around line 21-29: The current loop fetches ordersWithHoldInvoice before
acquiring PerOrderIdMutex, causing a TOCTOU race where another process may
change order.status; inside the runExclusive callback (for the same
String(order._id)) re-fetch the latest Order by id (e.g.,
Order.findById(order._id)), verify its status still matches the expected
precondition (the status you intended to expire, e.g., 'HOLD_INVOICE' or
whatever predicate you used to build ordersWithHoldInvoice), and only then set
status = 'HOLD_INVOICE_EXPIRED', await order.save(), and call
OrderEvents.orderUpdated(order); if the re-fetched order no longer meets the
precondition, skip updating. Ensure you reference
PerOrderIdMutex.instance.runExclusive, order._id, Order.findById (or
equivalent), order.save, and OrderEvents.orderUpdated.
- Around line 24-27: The job currently sets order.status to
'HOLD_INVOICE_EXPIRED' and emits OrderEvents.orderUpdated but misses cancelling
the hold invoice and notifying users; modify the handler in
jobs/check_hold_invoice_expired.ts to first call and await cancelHoldInvoice({
hash: order.hash }) (wrapped in try/catch so failures are logged but do not
block cleanup), then update order.status and save, and after saving send the
standard notifications to both parties using
messages.toSellerDidntPayInvoiceMessage() and
messages.toBuyerSellerDidntPayInvoiceMessage() (or
messages.expiredOrderMessage() where appropriate) so both buyer and seller are
explicitly informed; ensure the sequence matches other flows (cancelHoldInvoice
→ set status/save → send messages → OrderEvents.orderUpdated) and that calls are
idempotent.
🧹 Nitpick comments (1)
bot/start.ts (1)

242-244: The job doesn't receive the bot instance, limiting its ability to notify users.

Most other scheduled jobs (e.g., cancelOrders, deleteOrders, nodeInfo) receive the bot instance to send Telegram messages. If user notifications are added (as suggested in the job implementation review), the bot reference will be needed here.

Future-proof by passing bot
   schedule.scheduleJob(`0 * * * * *`, async () => {
-    await checkHoldInvoiceExpired();
+    await checkHoldInvoiceExpired(bot);
   });

Copy link
Contributor

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/bot/bot.spec.ts (1)

307-315: ⚠️ Potential issue | 🟡 Minor

Missing checkHoldInvoiceExpired stub in the ../jobs mock.

The ../jobs proxy (lines 307–315) doesn't include a stub for the newly added checkHoldInvoiceExpired job. While the scheduleJob call count was bumped to 8, the job function itself will be undefined in the mock, so the test doesn't actually verify that the new job is wired correctly.

Add the stub and assert it is scheduled with the expected cron expression (* * * * * — every minute, per the PR description):

Proposed fix
       '../jobs': {
         attemptPendingPayments: attemptPendingPaymentsStub,
         cancelOrders: sinon.stub().resolves(),
         deleteOrders: sinon.stub().resolves(),
         calculateEarnings: sinon.stub().resolves(),
         attemptCommunitiesPendingPayments: sinon.stub().resolves(),
         // deleteCommunity: sinon.stub().resolves(),
         nodeInfo: sinon.stub().resolves(),
+        checkHoldInvoiceExpired: sinon.stub().resolves(),
       },

And optionally verify the cron expression for the new job in the assertion block (after line 466):

     expect(scheduleStub.scheduleJob.callCount).to.be.equal(8);
+    // Verify the new hold-invoice-expired job is scheduled every minute
+    const calls = scheduleStub.scheduleJob.getCalls();
+    const holdInvoiceCall = calls.find((c: any) => c.args[0] === '* * * * *');
+    expect(holdInvoiceCall).to.not.be.undefined;

Based on learnings: "Add fixtures in tests/bot/mocks/ when altering localization or jobs."

Also applies to: 466-466

- Implemented a new job `checkHoldInvoiceExpired` to update orders with expired hold invoices.
- Scheduled the job to run every minute in the bot's initialization.
- Added a new status 'HOLD_INVOICE_EXPIRED' to the order model for tracking expired invoices.
@Luquitasjeffrey Luquitasjeffrey self-requested a review February 16, 2026 14:27
Copy link
Contributor

@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

🧹 Nitpick comments (2)
jobs/check_hold_invoice_expired.ts (1)

60-75: Notifications are silently skipped when buyer or seller is missing.

If either buyerUser or sellerUser is null, both notifications are skipped. In practice, both should always exist for orders in these statuses, but a log message would help diagnose issues if this ever happens.

Suggested improvement
             if (buyerUser !== null && sellerUser !== null) {
               // ... send messages ...
+            } else {
+              logger.warning(
+                `checkHoldInvoiceExpired: buyer or seller not found for order ${updatedOrder._id}`,
+              );
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@jobs/check_hold_invoice_expired.ts` around lines 60 - 75, The current
conditional skips both notifications if either buyerUser or sellerUser is null;
change the logic to handle each recipient independently: call getUserI18nContext
and messages.toBuyerHoldInvoiceExpiredMessage when buyerUser is non-null, and do
the same separately for sellerUser when non-null, and add a processLogger.warn
(or similar) when either buyerUser or sellerUser is missing to surface the
unexpected state; reference buyerUser, sellerUser, getUserI18nContext,
messages.toBuyerHoldInvoiceExpiredMessage,
messages.toSellerHoldInvoiceExpiredMessage, bot and updatedOrder when making the
changes.
tests/bot/bot.spec.ts (1)

466-466: Count bump is correct, but the '../jobs' mock is missing checkHoldInvoiceExpired.

The schedule count correctly increased to 8. However, the '../jobs' mock (lines 307-315) doesn't include a checkHoldInvoiceExpired stub. While this doesn't break the test (since scheduleJob is stubbed and never executes callbacks), adding it would make the mock complete and allow future assertions on the job's invocation.

Additionally, there's no dedicated unit test for the checkHoldInvoiceExpired job itself. Given that this job modifies order state, cancels hold invoices, and sends user notifications, unit tests would be valuable. Based on learnings, fixtures should be added in tests/bot/mocks/ when altering jobs.

Add missing stub
       '../jobs': {
         attemptPendingPayments: attemptPendingPaymentsStub,
         cancelOrders: sinon.stub().resolves(),
         deleteOrders: sinon.stub().resolves(),
         calculateEarnings: sinon.stub().resolves(),
         attemptCommunitiesPendingPayments: sinon.stub().resolves(),
         // deleteCommunity: sinon.stub().resolves(),
         nodeInfo: sinon.stub().resolves(),
+        checkSolvers: sinon.stub().resolves(),
+        checkHoldInvoiceExpired: sinon.stub().resolves(),
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/bot/bot.spec.ts` at line 466, The '../jobs' mock used in tests is
missing a stub for checkHoldInvoiceExpired; add a stub/export named
checkHoldInvoiceExpired alongside the existing mocked jobs (the same style as
the other job stubs around the mock block) so scheduleJob can be asserted
against it later, and add a new unit test for the checkHoldInvoiceExpired job
(exercise the checkHoldInvoiceExpired handler, assert it updates order state,
cancels hold invoices, and sends notifications) with required fixtures placed
into tests/bot/mocks/ per the existing fixture patterns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@jobs/check_hold_invoice_expired.ts`:
- Line 17: The job only updates order status for orders in orderStatuses
(['ACTIVE','DISPUTE','FIAT_SENT']) but does not update the related Dispute
record when an order in DISPUTE is moved to 'HOLD_INVOICE_EXPIRED', leaving
stale disputes; import the Dispute model/class in
jobs/check_hold_invoice_expired.ts and when you set an order's status to
'HOLD_INVOICE_EXPIRED' for orders that were 'DISPUTE', also find the associated
Dispute (by orderId or dispute.orderId) and update its status to
'SELLER_REFUNDED' (same flow as cancelorder) and save the change so solvers'
queues are cleared.

---

Nitpick comments:
In `@jobs/check_hold_invoice_expired.ts`:
- Around line 60-75: The current conditional skips both notifications if either
buyerUser or sellerUser is null; change the logic to handle each recipient
independently: call getUserI18nContext and
messages.toBuyerHoldInvoiceExpiredMessage when buyerUser is non-null, and do the
same separately for sellerUser when non-null, and add a processLogger.warn (or
similar) when either buyerUser or sellerUser is missing to surface the
unexpected state; reference buyerUser, sellerUser, getUserI18nContext,
messages.toBuyerHoldInvoiceExpiredMessage,
messages.toSellerHoldInvoiceExpiredMessage, bot and updatedOrder when making the
changes.

In `@tests/bot/bot.spec.ts`:
- Line 466: The '../jobs' mock used in tests is missing a stub for
checkHoldInvoiceExpired; add a stub/export named checkHoldInvoiceExpired
alongside the existing mocked jobs (the same style as the other job stubs around
the mock block) so scheduleJob can be asserted against it later, and add a new
unit test for the checkHoldInvoiceExpired job (exercise the
checkHoldInvoiceExpired handler, assert it updates order state, cancels hold
invoices, and sends notifications) with required fixtures placed into
tests/bot/mocks/ per the existing fixture patterns.

- Updated the `checkHoldInvoiceExpired` job to handle orders in 'DISPUTE' status.
- Modified the query to include 'DISPUTE' in the list of order statuses with expired hold invoices.
- Added logic to update the corresponding dispute status to 'SELLER_REFUNDED' when an order in dispute is processed.
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.

2 participants