Skip to content

fix(payments): back-fill orphaned payment records via Horizon in reconciliation job#34

Open
Jaydbrown wants to merge 2 commits into
OrbitStream:mainfrom
Jaydbrown:fix/issue-18-atomic-payment-processing
Open

fix(payments): back-fill orphaned payment records via Horizon in reconciliation job#34
Jaydbrown wants to merge 2 commits into
OrbitStream:mainfrom
Jaydbrown:fix/issue-18-atomic-payment-processing

Conversation

@Jaydbrown

Copy link
Copy Markdown

Closes #18

Summary

The recoverPaidWithoutPayments path in the reconciliation job found sessions marked paid with no corresponding payments row but only logged them as "requires manual investigation" — it never actually recovered them. This PR makes that path active: it queries Horizon to find the confirming transaction, verifies it, and inserts the missing payment record so the DB is consistent without manual intervention.

Root cause

When the phase-2 transaction in processPayment (payment insert + paid status update) commits the session status update but the process dies before the payments insert completes — or the insert is rolled back while the outer status update already committed — you get a paid session with no payment row. The reconciliation job found these but stopped at a log message.

Changes

src/payments/payment-recovery.service.ts

PaidWithoutPaymentRow interface — added memo and receiving_account columns to the SELECT so we have the data needed to query Horizon:

// Before
interface PaidWithoutPaymentRow {
  id: string;
  merchant_id: string;
  amount: string;
  asset_code: string;
}

// After
interface PaidWithoutPaymentRow {
  id: string;
  merchant_id: string;
  amount: string;
  asset_code: string;
  memo: string | null;
  receiving_account: string;
}

findConfirmingPayment — widened the parameter type to accept both StuckProcessingRow and PaidWithoutPaymentRow, avoiding code duplication:

private async findConfirmingPayment(
  row: StuckProcessingRow | PaidWithoutPaymentRow,
): Promise<any | null>

recoverPaidWithoutPayments — now actively recovers instead of just logging:

// Before: only logged a warning
for (const row of paidOrphans) {
  this.logger.warn(`Paid session ${row.id} ... requires manual investigation`);
}

// After: queries Horizon, verifies, inserts missing payment record
for (const row of paidOrphans) {
  const match = row.memo ? await this.findConfirmingPayment(row) : null;

  if (!match) {
    this.logger.warn(`... no matching Horizon payment found; requires manual investigation`);
    continue;
  }

  const confirmed = await this.stellar.verifyTransaction(match.transaction_hash);
  if (!confirmed) {
    this.logger.warn(`... Horizon tx not successful; requires manual investigation`);
    continue;
  }

  await db.insert(payments).values({ ... })
    .onConflictDoNothing({ target: [payments.txHash, payments.sessionId] });

  this.logger.log(`Back-filled payment record for session ${row.id} — Horizon tx ${match.transaction_hash}`);
}

onConflictDoNothing ensures the insert is idempotent — if the cron fires twice before the session is resolved, the second run is a no-op.

Recovery flow after this fix

paid session, no payments row
        │
        ▼
  has memo? ──No──▶ log for manual review
        │
       Yes
        │
        ▼
  findConfirmingPayment() → Horizon
        │
   not found ──────────▶ log for manual review
        │
      found
        │
        ▼
  verifyTransaction()
        │
  not confirmed ────────▶ log for manual review
        │
    confirmed
        │
        ▼
  INSERT INTO payments
  (onConflictDoNothing)
        │
        ▼
  log "Back-filled payment record"

Test plan

  • Run reconciliation job with a paid session that has no payments row and a matching Horizon transaction → payment record is inserted
  • Run again immediately → second insert is a no-op (idempotent)
  • Session with no memo → logged for manual review, no crash
  • Session where Horizon returns no matching transaction → logged for manual review
  • Session where verifyTransaction returns false → logged for manual review

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.

[BUG] Atomic Payment Processing + Reconciliation

2 participants