import "server-only"; import { paymentSnapshotSchema, type PaymentSnapshot } from "@salixcommerce/core"; import { PostgrestClient } from "@supabase/postgrest-js"; import { paymentFailure, type PaymentRepository, type PaymentResult } from "./repository"; interface SupabasePaymentCredentials { readonly serviceRoleKey: string; readonly url: string; } type DatabaseRow = Record; export function createSupabasePaymentRepository( credentials: SupabasePaymentCredentials, ): PaymentRepository { const client = new PostgrestClient(`${credentials.url.replace(/\/$/u, "")}/rest/v1`, { headers: { apikey: credentials.serviceRoleKey, authorization: `Bearer ${credentials.serviceRoleKey}`, }, timeout: 10_000, }); async function loadPaymentById(paymentId: string): Promise { const paymentResult = await client .from("payments") .select( "id,order_id,provider,provider_payment_reference,checkout_url,status,currency,amount_minor,refunded_amount_minor,created_at,updated_at", ) .eq("id", paymentId) .limit(1); const payments = readRows(paymentResult.data, paymentResult.error, "payment"); const payment = payments[0]; if (payment === undefined) return null; const orderResult = await client .from("orders") .select("reference") .eq("id", readIdentifier(payment, "order_id")) .limit(1); const orders = readRows(orderResult.data, orderResult.error, "payment order"); const order = orders[0]; if (order === undefined) throw new Error("Payment order is missing."); return parsePayment(payment, readString(order, "reference")); } const repository = { async applyEvent(input) { const event = input.event; const result = await client.rpc("salix_apply_payment_event", { p_amount_minor: event.amount.amountMinor, p_currency: event.amount.currency, p_event_hash: input.eventHash, p_kind: event.kind, p_occurred_at: event.occurredAt, p_provider: event.provider, p_provider_event_id: event.eventId, p_provider_payment_reference: event.providerPaymentReference, }); const rpc = readRpcResult(result.data, result.error); if (!rpc.ok) return rpc; const payment = await loadPaymentById(readIdentifier(rpc.data, "payment_id")); if (payment === null) return providerFailure(); const eventResult = readString(rpc.data, "event_result"); const orderStatus = readString(rpc.data, "order_status"); if (eventResult !== "applied" && eventResult !== "ignored") return providerFailure(); if ( orderStatus !== "pending_payment" && orderStatus !== "paid" && orderStatus !== "processing" && orderStatus !== "fulfilled" && orderStatus !== "cancelled" ) { return providerFailure(); } return { data: { eventResult, orderReference: readString(rpc.data, "order_reference"), orderStatus, payment, replayed: readBoolean(rpc.data, "replayed"), }, ok: true, }; }, async loadAuthorizedPayment(capabilityHash, provider, providerPaymentReference) { const paymentResult = await client .from("payments") .select( "id,order_id,provider,provider_payment_reference,checkout_url,status,currency,amount_minor,refunded_amount_minor,created_at,updated_at", ) .eq("provider", provider) .eq("provider_payment_reference", providerPaymentReference) .limit(1); const payments = readRows(paymentResult.data, paymentResult.error, "authorized payment"); const payment = payments[0]; if (payment === undefined) return null; const orderResult = await client .from("orders") .select("reference") .eq("id", readIdentifier(payment, "order_id")) .eq("capability_hash", capabilityHash) .limit(1); const orders = readRows(orderResult.data, orderResult.error, "authorized payment order"); const order = orders[0]; return order === undefined ? null : parsePayment(payment, readString(order, "reference")); }, async loadLatestPayment(capabilityHash, orderReference) { const orderResult = await client .from("orders") .select("id") .eq("capability_hash", capabilityHash) .eq("reference", orderReference) .limit(1); const orders = readRows(orderResult.data, orderResult.error, "payment order"); const order = orders[0]; if (order === undefined) return null; const paymentResult = await client .from("payments") .select( "id,order_id,provider,provider_payment_reference,checkout_url,status,currency,amount_minor,refunded_amount_minor,created_at,updated_at", ) .eq("order_id", readIdentifier(order, "id")) .order("created_at", { ascending: false }) .order("id", { ascending: false }) .limit(1); const payments = readRows(paymentResult.data, paymentResult.error, "latest payment"); const payment = payments[0]; return payment === undefined ? null : parsePayment(payment, orderReference); }, async recordCheckout(input) { const checkout = input.checkout; const result = await client.rpc("salix_record_payment_checkout", { p_amount_minor: checkout.amount.amountMinor, p_capability_hash: input.capabilityHash, p_checkout_url: checkout.checkoutUrl, p_currency: checkout.amount.currency, p_idempotency_key: input.idempotencyKey, p_order_reference: checkout.orderReference, p_provider: checkout.provider, p_provider_payment_reference: checkout.providerPaymentReference, p_request_hash: input.requestHash, }); const rpc = readRpcResult(result.data, result.error); if (!rpc.ok) return rpc; const payment = await loadPaymentById(readIdentifier(rpc.data, "payment_id")); if (payment === null) return providerFailure(); return { data: { payment, replayed: readBoolean(rpc.data, "replayed") }, ok: true, }; }, } satisfies PaymentRepository; return Object.freeze(repository); } function parsePayment(row: DatabaseRow, orderReference: string): PaymentSnapshot { const currency = readString(row, "currency"); return paymentSnapshotSchema.parse({ amount: { amountMinor: readSafeNumber(row, "amount_minor"), currency }, checkoutUrl: readString(row, "checkout_url"), createdAt: readString(row, "created_at"), id: readIdentifier(row, "id"), orderReference, provider: readString(row, "provider"), providerPaymentReference: readString(row, "provider_payment_reference"), refundedAmount: { amountMinor: readSafeNumber(row, "refunded_amount_minor"), currency }, status: readString(row, "status"), updatedAt: readString(row, "updated_at"), }); } function readRpcResult(data: unknown, error: unknown): PaymentResult { if (error !== null) return { error: mapProviderError(error), ok: false }; if (!Array.isArray(data) || data.length !== 1 || !isDatabaseRow(data[0])) { return providerFailure(); } return { data: data[0], ok: true }; } function mapProviderError(error: unknown) { const message = isDatabaseRow(error) && typeof error.message === "string" ? error.message : ""; const known = { SALIX_IDEMPOTENCY_CONFLICT: paymentFailure( "IDEMPOTENCY_CONFLICT", "That payment request key was already used for different input.", 409, ), SALIX_ILLEGAL_ORDER_TRANSITION: paymentFailure( "ILLEGAL_PAYMENT_TRANSITION", "This order cannot accept that payment event.", 409, ), SALIX_ORDER_NOT_FOUND: paymentFailure("ORDER_NOT_FOUND", "Order not found.", 404), SALIX_PAYMENT_ALREADY_PENDING: paymentFailure( "PAYMENT_ALREADY_PENDING", "This order already has a payment in progress.", 409, ), SALIX_PAYMENT_AMOUNT_MISMATCH: paymentFailure( "PAYMENT_AMOUNT_MISMATCH", "The verified payment does not match the canonical order amount.", 409, ), SALIX_PAYMENT_EVENT_CONFLICT: paymentFailure( "PAYMENT_EVENT_CONFLICT", "A provider event identifier was reused with different content.", 409, ), SALIX_PAYMENT_NOT_FOUND: paymentFailure("PAYMENT_NOT_FOUND", "Payment not found.", 404), SALIX_REFUND_EXCEEDS_PAYMENT: paymentFailure( "REFUND_EXCEEDS_PAYMENT", "The refund exceeds the captured payment amount.", 409, ), } as const; if (message in known) return known[message as keyof typeof known]; if (message.startsWith("SALIX_INVALID_")) { return paymentFailure("INVALID_REQUEST", "The payment request is invalid.", 400); } return paymentFailure( "PROVIDER_FAILURE", "The payment service could not complete the request.", 500, ); } function providerFailure(): PaymentResult { return { error: paymentFailure( "PROVIDER_FAILURE", "The payment service returned an invalid response.", 500, ), ok: false, }; } function readRows(data: unknown, error: unknown, label: string): DatabaseRow[] { if (error !== null) throw new Error(`Unable to load ${label} from the payment store.`); if (!Array.isArray(data) || !data.every(isDatabaseRow)) { throw new TypeError(`Payment store returned invalid ${label}.`); } return data; } function isDatabaseRow(value: unknown): value is DatabaseRow { return typeof value === "object" && value !== null && !Array.isArray(value); } function readString(row: DatabaseRow, key: string): string { const value = row[key]; if (typeof value !== "string") throw new TypeError(`Payment field ${key} must be a string.`); return value; } function readBoolean(row: DatabaseRow, key: string): boolean { const value = row[key]; if (typeof value !== "boolean") throw new TypeError(`Payment field ${key} must be boolean.`); return value; } function readIdentifier(row: DatabaseRow, key: string): string { const value = row[key]; if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return String(value); if (typeof value === "string" && /^[1-9][0-9]*$/u.test(value)) return value; throw new TypeError(`Payment field ${key} must be a positive safe identifier.`); } function readSafeNumber(row: DatabaseRow, key: string): number { const value = row[key]; const parsed = typeof value === "string" && /^-?[0-9]+$/u.test(value) ? Number(value) : value; if (typeof parsed !== "number" || !Number.isSafeInteger(parsed)) { throw new TypeError(`Payment field ${key} must be a safe integer.`); } return parsed; }