{"record":{"id":"9d4f845c93cd54ef","repo":"moeru-ai/airi","slug":"internal-server-error-payment-order-not-found","errorCode":"INTERNAL_SERVER_ERROR","errorMessage":"Payment order not found","messagePattern":"Payment order not found","errorType":"http","errorClass":"ApiError","httpStatus":500,"severity":"error","filePath":"server/apps/api/src/services/domain/payment/index.ts","lineNumber":95,"sourceCode":"        eq(schema.paymentCustomer.userId, userId),\n        eq(schema.paymentCustomer.processor, processor),\n        isNull(schema.paymentCustomer.deletedAt),\n      ))\n      .limit(1)\n\n    return customer?.customerId\n  }\n\n  async function claimExistingOrder(receipt: ClaimReceipt): Promise<SettleResult> {\n    const result = await db.transaction(async (tx) => {\n      const [order] = await tx\n        .select()\n        .from(schema.paymentOrder)\n        .where(eq(schema.paymentOrder.id, receipt.paymentOrderId))\n        .for('update')\n\n      if (!order)\n        throw createInternalError('Payment order not found')\n\n      if (order.processor !== receipt.processor || (order.processorOrderId && order.processorOrderId !== receipt.processorOrderId))\n        throw createInternalError('Payment receipt does not match order')\n\n      if (order.deletedAt)\n        return { applied: false as const }\n\n      switch (receipt.status) {\n        case 'paid': {\n          if (order.status === 'paid')\n            return { applied: false as const }\n\n          if (order.status !== 'pending')\n            return { applied: false as const }\n\n          // NOTICE:\n          // Old and new replicas must claim the same retained checkout row.\n          // Migration 0023 is a snapshot; an old webhook can credit after it.","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/server/apps/api/src/services/domain/payment/index.ts#L77-L113","documentation":"claimExistingOrder locks a payment_order row FOR UPDATE using the paymentOrderId embedded in a Stripe webhook ClaimReceipt. This error means the SELECT FOR UPDATE returned no row: the order id referenced by the receipt does not exist in payment_order. The service throws createInternalError because a settleable webhook pointing at a missing order indicates corrupted data or a receipt built from a foreign/legacy row, so it refuses to guess.","triggerScenarios":"A Stripe webhook settles a receipt whose paymentOrderId is not present in payment_order: the order row was hard-deleted or never inserted (openPending insert failed before bindProcessorOrder), the receipt was reconstructed from a legacy stripe_checkout_session row that predates the payment_order table, or a test/manual replay uses a fabricated order id.","commonSituations":"Running an old database without the payment_order migration (0023) while new webhook code is deployed; replaying old Stripe webhook events from the dashboard after restoring or truncating tables; purging test orders while live webhooks still reference them; misconfigured environment pointing the API at a different database than the one that created the order.","solutions":["Verify the payment_order row exists for receipt.paymentOrderId: SELECT * FROM payment_order WHERE id = '<receipt.paymentOrderId>'. If missing, confirm which database the API instance connects to and fix DATABASE_URL.","Replay the webhook only after the order row exists, or let Stripe retry: fix the data first, then use Stripe CLI `stripe events retry evt_...` or the dashboard to redeliver.","Check that migration 0023 (payment_order snapshot) was applied on every deployed instance; run `pnpm -F @proj-airi/api-server db:generate`/`db:push` or the drizzle migration step in deploy.","If the receipt comes from a legacy stripe_checkout_session flow, ensure the legacy session row has a valid, backfilled payment_order id before settling.","Do not fabricate order ids in tests or manual replays; create the order via openPending first."],"exampleFix":"// before (manual replay of a stale webhook)\nsettle({ paymentOrderId: 'order-from-old-db', status: 'paid', ... })\n\n// after (verify existence first)\nconst [order] = await db.select().from(paymentOrder).where(eq(paymentOrder.id, receipt.paymentOrderId))\nif (!order) {\n  logger.warn('skipping settle: order missing', { orderId: receipt.paymentOrderId })\n  return // do not settle; investigate DB/env instead of throwing\n}","handlingStrategy":"try-catch","validationCode":"const [order] = await db.select({ id: paymentOrder.id }).from(paymentOrder).where(eq(paymentOrder.id, receipt.paymentOrderId)).limit(1)\nif (!order)\n  throw new Error(`order ${receipt.paymentOrderId} missing; skip settle`)","typeGuard":"function isSettleableOrder(order: PaymentOrder | undefined): order is PaymentOrder {\n  return order != null && order.deletedAt == null\n}","tryCatchPattern":"try {\n  await payment.settle(receipt)\n}\ncatch (error) {\n  if (errorMessageFrom(error)?.includes('Payment order not found')) {\n    logger.warn('settle skipped: order missing, likely stale/legacy webhook', { orderId: receipt.paymentOrderId })\n    return // do not retry until data is fixed\n  }\n  throw error\n}","preventionTips":["Ensure migration 0023 (payment_order) is applied before deploying webhook settle code","Reconcile legacy stripe_checkout_session rows to backfilled payment_order ids before replaying old webhooks","Point all deployed API instances at the same DATABASE_URL/database","Never hard-delete payment_order rows; use deletedAt soft delete as the code expects","Add an integration test settling a receipt for a nonexistent order id"],"tags":["database","stripe","webhook","data-integrity"],"backgroundTag":"record-not-found","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}