Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# NOW -- The invoice is claimed before it is credited (2026-09-17)

## What was read

- `payment-ai-server-process` (999-multibots-telegraf) credited an invoice by `inv_id` through `updateUserBalance`: status set to COMPLETED unconditionally, `true` returned even when no row matched, and the amount in the event never compared with the amount that was invoiced.
- The Robokassa ResultURL route settles the same `payments_v2` rows with a compare-and-set on PENDING and credits only when it wins. The Inngest path had no such claim.
- The events of this function have no sender inside the repo (orphan-events baseline); who sends them was not established.

## What this spec says

- STEPS gains `claim-invoice` between `get-bot-config` and `update-user-balance`.
- The row must exist and its RUB amount must equal the rounded IncSum, else the run ends with NonRetriableError and the admin chat hears about it; nothing is credited.
- The PENDING -> COMPLETED flip is a compare-and-set; a lost race or an already COMPLETED row returns `already_claimed` without crediting or notifying.
- A database error on the claim stays retriable.

## Not verified

- The sender of `payment/ai-server.process`; the live path was not exercised.
- Behaviour on the deployed build: needs a deploy and a real or staged invoice.

## Where the code goes

- 999-multibots-telegraf, branch `payment-claim-invoice`: `core/supabase/claimPendingInvoice.ts`, `paymentProcessing.ts`, `functions.manifest.json`, test `money/paymentClaimInvoice.test.ts` citing this spec.
6 changes: 3 additions & 3 deletions specs/functions/payment-ai-server-process.t27
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub const LEGACY_ID : str = "payment-processing-ai-server";
pub const NAME : str = "AI-server payment processing";
pub const REPO : str = "999-multibots-telegraf";
; file:line of inngest.createFunction( in the repo named in REPO
pub const SERVICE : str = "src/inngest_app/functions/payments/paymentProcessing.ts:64";
pub const SERVICE : str = "src/inngest_app/functions/payments/paymentProcessing.ts:66";
pub const DOMAIN : str = "payment";
; "event" or "cron"; an event function has EVENT (+ LEGACY_EVENTS), a cron function has CRON
pub const TRIGGER : str = "event";
Expand All @@ -26,7 +26,7 @@ pub const CRON : str = "";
pub const TZ : str = "UTC";
pub const SUMMARY_EN : str = "Processes a payment reported by the AI server: matches the amount to a subscription plan or a payment option, loads the user and the bot configuration, updates the user balance and sends the payment notification.";
; step.run names in source order
pub const STEPS : [6]str = ["check-subscription-plan", "check-payment-option", "get-user-info", "get-bot-config", "update-user-balance", "send-notification"];
pub const STEPS : [7]str = ["check-subscription-plan", "check-payment-option", "get-user-info", "get-bot-config", "claim-invoice", "update-user-balance", "send-notification"];
; retries declared on the function; when the code declares none, the Inngest JS SDK v3 default (4) is written and NOTE says so
pub const RETRIES : u8 = 3;
; "admin-telegram" (onFailure handler messages the admin chat), "log" (no handler) or "refund+notify"
Expand All @@ -39,4 +39,4 @@ pub const GUARD : str = "amount-match";
pub const SAFE_PROBE : str = "{\"IncSum\": \"0.01\", \"inv_id\": \"e2e-safe-mode-probe\"}";
pub const PROBE_RESULT : str = "COMPLETED";
pub const CONTROL : str = "spec+code";
pub const NOTE : str = "The manifest carries no file, steps or retries for this function (its extractor did not resolve them); SERVICE, STEPS and RETRIES here were read from the same checkout, where retries is declared as 3 and onFailure is createInngestFailureHandler, which logs and messages the admin chat -- the manifest says on_failure log. Guard is amount-match: an amount matching no plan or option stops the run, which is why the 2026-09-09 probe with IncSum 0.01 COMPLETED without a balance change.";
pub const NOTE : str = "The manifest carries no file, steps or retries for this function (its extractor did not resolve them); SERVICE, STEPS and RETRIES here were read from the same checkout, where retries is declared as 3 and onFailure is createInngestFailureHandler, which logs and messages the admin chat -- the manifest says on_failure log. Guard is amount-match: an amount matching no plan or option stops the run, which is why the 2026-09-09 probe with IncSum 0.01 COMPLETED without a balance change. 2026-09-17 (money loop, plan section 6 item 3): a claim-invoice step now sits between get-bot-config and update-user-balance (core/supabase/claimPendingInvoice.ts). The payments_v2 row must exist and its amount (parseFloat(OutSum), RUB) must equal the rounded IncSum, else NonRetriableError -- no credit, the onFailure handler messages the admin chat. The PENDING -> COMPLETED flip is a compare-and-set (.eq status PENDING, .select); a lost race or an already COMPLETED row returns {success:true, already_claimed:true} without crediting or notifying. A database error on the claim stays a plain Error so the 3 retries have it. Until then updateUserBalance set COMPLETED by inv_id unconditionally and returned true even when no row matched, and the event amount was never compared with the invoice. The Robokassa ResultURL route settles the same rows with the same compare-and-set, so between the two paths an invoice credits at most once. Not verified: the events for this function have no sender in the repo (orphan-events baseline); who sends them was not established. Test: 999-multibots-telegraf src/__tests__/money/paymentClaimInvoice.test.ts.";
Loading