Skip to content

Indexer Handler: InvoicePaidEvent → Invoice State Sync #30

Description

@codebestia

Blocked by: Issue #24 (Core Soroban Indexer Infrastructure). This issue registers a handler against the dispatch registry, cursor, and dedupe pipeline that Issue #24 — it cannot start until that skeleton exists.

Background

Issue #24 builds the generic decode → dedupe → dispatch pipeline with an empty handler registry. This issue plugs in the first real handler: InvoicePaidEvent, following the same decode pattern proven in the earlier proof of concept (topic decoded via scValToNative) — but this time the Prisma writes live in a service function the handler calls, not inline in the handler itself, so the state-mutation logic is testable on its own and independent of the indexer plumbing.

Proposed Steps

  1. Confirm the real topic symbol and decoded field casing for InvoicePaidEvent against testnet — this should already exist from Issue Core Soroban Indexer Infrastructure #24's verification step; don't re-guess it here, and don't proceed with a placeholder topic string.
  2. Add the decoded shape to src/indexer/types.ts (if Issue Core Soroban Indexer Infrastructure #24 didn't already scaffold it): InvoicePaidEventData { invoiceId, merchantId, payer, amount, fee, merchantAmount, token, timestamp }.
  3. Add a service function — applyInvoicePayment(event: InvoicePaidEventData, txHash: string) in src/services/invoice.services.ts, alongside the invoice model's other mutations — responsible for:
    • Looking up Invoice by invoiceId (log and skip if not found — don't throw; ordering gaps between DB and chain are possible and not an error condition)
    • Looking up Merchant by merchantId
    • Computing amountPaid = invoice.amountPaid + event.amount and the resulting status: PAID if amountPaid >= invoice.amount, else PARTIALLY_PAID
    • Inside one prisma.$transaction: updating the Invoice (status, payer, amountPaid, datePaid) and creating a Transaction row (transactionType: INVOICE_PAYMENT, refId: invoiceId, amount, token, merchantId, date)
  4. Add src/indexer/handlers/invoicePaid.ts — thin glue only: receives the already-decoded event from the poller and calls applyInvoicePayment. No Prisma calls in this file directly.
  5. Register it: registerEventHandler(<confirmed topic string>, handleInvoicePaid) at whatever bootstrap point Issue Core Soroban Indexer Infrastructure #24's registry expects (e.g. imported by run.ts before startPolling() is called).
  6. Idempotency is already handled upstream by Issue Core Soroban Indexer Infrastructure #24's IndexerEvent dedupe table, before dispatch ever reaches this handler — do not add a second replay guard here (e.g. no Transaction.eventId-style uniqueness needed).
  7. Once this handler is verified working end-to-end on testnet, deprecate POST /pay/:slug/confirm for the wallet-payment path: stop treating its input as authoritative for invoice status — either remove the route, or downgrade it to a non-authoritative log-only call that no longer mutates Invoice.status/amountPaid. Do not remove it before this handler is proven working; there would be no confirmation path left in the interim.
  8. Do not implement deposit-based detection here. Leave a clear note (code comment or follow-up ticket reference) that it's a known gap pending a future issue.

Acceptance Criteria

  • Handler is registered using the topic string confirmed against a real testnet event, not a placeholder
  • All Prisma writes live in applyInvoicePayment (invoice.services.ts); handlers/invoicePaid.ts contains no direct Prisma calls
  • Triggering pay_invoice on testnet results in the matching Invoice.status / amountPaid / datePaid being updated and a Transaction row created — with no client-submitted confirmation involved
  • invoiceId not found in the DB → logged and skipped, not thrown
  • A partial payment (event amount less than the remaining balance) results in status = PARTIALLY_PAID
  • A payment that completes the invoice results in status = PAID and datePaid set
  • No second idempotency/dedupe mechanism is introduced in this issue — replay protection is entirely Issue Core Soroban Indexer Infrastructure #24's IndexerEvent pipeline's responsibility
  • POST /pay/:slug/confirm is deprecated/de-authoritative for wallet payments once this handler is verified working — only after, not before
  • Deposit-based confirmation is explicitly not implemented here
  • No new Prisma models or fields required

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions