import { basename, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { definePlugin, type PluginContext } from "@wrnexus/plugin"; import { configurePaymentFromConfig, validatePaymentConfig, type PaymentConfig } from "./config.ts"; import { configurePaymentApi } from "./http.ts"; import { databasePaymentStore } from "./database.ts"; const root = dirname(dirname(fileURLToPath(import.meta.url))); const extension = basename(dirname(fileURLToPath(import.meta.url))) === "dist" ? ".js" : ".ts"; const webhookEntry = join(dirname(fileURLToPath(import.meta.url)), `runtime-webhook${extension}`); const apiEntry = join(dirname(fileURLToPath(import.meta.url)), `runtime-api${extension}`); const apiRoutes = [ "/api/payments/providers", "/api/payments/intents", "/api/payments/intents/[id]", "/api/payments/intents/[id]/sync", "/api/payments/intents/[id]/capture", "/api/payments/intents/[id]/cancel", "/api/payments/intents/[id]/refund", "/api/payments/methods", "/api/payments/reconcile", ]; const migration = `-- +up CREATE TABLE IF NOT EXISTS wrn_payment_intent (id TEXT PRIMARY KEY, subject TEXT NOT NULL, gateway TEXT NOT NULL, gateway_ref TEXT NOT NULL, amount BIGINT NOT NULL, currency TEXT NOT NULL, idempotency_key TEXT NOT NULL UNIQUE, created_at BIGINT NOT NULL, checkout_url TEXT, client_json TEXT NOT NULL, metadata_json TEXT NOT NULL); CREATE INDEX IF NOT EXISTS wrn_payment_intent_subject ON wrn_payment_intent(subject, created_at); CREATE TABLE IF NOT EXISTS wrn_payment_event (id TEXT PRIMARY KEY, intent_id TEXT NOT NULL, type TEXT NOT NULL, status TEXT NOT NULL, created_at BIGINT NOT NULL, payload_json TEXT, FOREIGN KEY(intent_id) REFERENCES wrn_payment_intent(id)); CREATE INDEX IF NOT EXISTS wrn_payment_event_intent ON wrn_payment_event(intent_id, created_at, id); CREATE TABLE IF NOT EXISTS wrn_payment_refund (id TEXT PRIMARY KEY, intent_id TEXT NOT NULL, gateway_ref TEXT NOT NULL, amount BIGINT NOT NULL, currency TEXT NOT NULL, reason TEXT NOT NULL, idempotency_key TEXT NOT NULL UNIQUE, status TEXT NOT NULL, created_at BIGINT NOT NULL, FOREIGN KEY(intent_id) REFERENCES wrn_payment_intent(id)); CREATE TABLE IF NOT EXISTS wrn_payment_method (id TEXT PRIMARY KEY, subject TEXT NOT NULL, gateway TEXT NOT NULL, gateway_ref TEXT NOT NULL, brand TEXT, last4 TEXT, expires TEXT, is_default INTEGER NOT NULL DEFAULT 0, created_at BIGINT NOT NULL); CREATE TABLE IF NOT EXISTS wrn_payment_customer (subject TEXT NOT NULL, gateway TEXT NOT NULL, gateway_ref TEXT NOT NULL, created_at BIGINT NOT NULL, PRIMARY KEY(subject,gateway)); -- +down DROP TABLE IF EXISTS wrn_payment_customer; DROP TABLE IF EXISTS wrn_payment_method; DROP TABLE IF EXISTS wrn_payment_refund; DROP TABLE IF EXISTS wrn_payment_event; DROP TABLE IF EXISTS wrn_payment_intent;`; const orderingMigration = `-- +up ALTER TABLE wrn_payment_event ADD COLUMN occurred_at BIGINT; ALTER TABLE wrn_payment_event ADD COLUMN gateway_sequence TEXT; UPDATE wrn_payment_event SET occurred_at = created_at WHERE occurred_at IS NULL; CREATE INDEX IF NOT EXISTS wrn_payment_event_gateway_order ON wrn_payment_event(intent_id, occurred_at, gateway_sequence, id); -- +down DROP INDEX IF EXISTS wrn_payment_event_gateway_order;`; export function paymentPlugin() { const key = "@wrnexus/payment:enabled"; return definePlugin({ name: "@wrnexus/payment", version: "0.8.5", componentDirs: [join(root, "components")], styleSources: [ { id: "payment-components", entry: join(root, "components", "payment.css"), source: join(root, "components"), order: "normal", }, ], routeEntries(context) { if (!context.metadata.get(key)) return []; const apiEnabled = context.metadata.get("@wrnexus/payment:api") !== false; return [ { kind: "api" as const, path: "/api/payments/webhook/:gateway", entry: webhookEntry }, ...(apiEnabled ? apiRoutes.map((path) => ({ kind: "api" as const, path, entry: apiEntry })) : []), ]; }, configure(config, context) { const value = config.payment as PaymentConfig | undefined; context.metadata.set(key, Boolean(value)); if (!value) return; const api = value.api === false ? { enabled: false } : (value.api ?? {}); context.metadata.set( "@wrnexus/payment:api", Boolean(value.gateways) && api.enabled !== false, ); configurePaymentApi(api); if (value.gateways) { validatePaymentConfig(value); configurePaymentFromConfig(value, value.store ?? databasePaymentStore()); } if (!value.default) throw new Error("WRN-PAYMENT-CONFIG: payment.default is required"); const legacySandbox = value.default === "sandbox" && !value.gateways; if (!value.gateways?.[value.default] && !legacySandbox) throw new Error( `WRN-PAYMENT-CONFIG: default gateway '${value.default}' has no configuration`, ); if ( process.env.NODE_ENV === "production" && (legacySandbox || Object.values(value.gateways ?? {}).some( (gateway) => gateway && gateway.provider === "sandbox", )) ) throw new Error("WRN-PAYMENT-CONFIG: sandbox gateways cannot be enabled in production"); }, migrations(context: PluginContext) { return context.metadata.get(key) ? [ { id: "wrnexus-payment-001", source: migration }, { id: "wrnexus-payment-002-event-order", source: orderingMigration }, ] : []; }, documentation: [join(root, "README.md")], }); } export default paymentPlugin;