@open-mercato/checkout 0.6.7-develop.6842.1.8cd8a8883c → 0.6.7-develop.6859.1.e4727b568a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- [build:checkout] found 160 entry points
1
+ [build:checkout] found 162 entry points
2
2
  [build:checkout] built successfully
@@ -0,0 +1,62 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { getAuthToken, apiRequest } from "@open-mercato/core/modules/core/__integration__/helpers/api";
3
+ import { readJsonSafe } from "@open-mercato/core/modules/core/__integration__/helpers/generalFixtures";
4
+ import {
5
+ createFixedTemplateInput,
6
+ createLinkFixture,
7
+ deleteCheckoutEntityIfExists,
8
+ findGatewayTransactionIdForCheckout,
9
+ readCheckoutTransaction,
10
+ sendMockGatewayWebhook,
11
+ submitPayLink,
12
+ waitForCheckoutStatus
13
+ } from "./helpers/fixtures.js";
14
+ test.describe("TC-CHKT-043 (concurrency): Two-contender race \u2014 only one updateStatus call wins", () => {
15
+ test("concurrent completed + processing status updates: one wins, one gets 409, completionCount incremented once", async ({ request }) => {
16
+ let token = null;
17
+ let linkId = null;
18
+ try {
19
+ token = await getAuthToken(request);
20
+ const link = await createLinkFixture(request, token, {
21
+ ...createFixedTemplateInput({ status: "active", collectCustomerDetails: false })
22
+ });
23
+ linkId = link.id;
24
+ const submitResponse = await submitPayLink(request, link.slug, {
25
+ customerData: {},
26
+ acceptedLegalConsents: {},
27
+ amount: 49.99
28
+ });
29
+ expect(submitResponse.status(), `Submit failed: ${submitResponse.status()}`).toBe(201);
30
+ const submitBody = await submitResponse.json();
31
+ const transactionId = submitBody.transactionId;
32
+ expect(transactionId).toBeTruthy();
33
+ const gatewayTransactionId = await findGatewayTransactionIdForCheckout(request, token, transactionId);
34
+ const gatewayTransaction = await apiRequest(request, "GET", `/api/payment_gateways/transactions/${encodeURIComponent(gatewayTransactionId)}`, { token });
35
+ const gtBody = await readJsonSafe(gatewayTransaction);
36
+ const providerSessionId = gtBody?.transaction?.providerSessionId;
37
+ expect(providerSessionId, "providerSessionId is required for webhook simulation").toBeTruthy();
38
+ const [webhookResult, directResult] = await Promise.allSettled([
39
+ sendMockGatewayWebhook(request, token, providerSessionId, "captured", 49.99),
40
+ apiRequest(request, "POST", `/api/checkout/transactions/${encodeURIComponent(transactionId)}/status`, {
41
+ token,
42
+ data: { status: "processing", paymentStatus: "processing" }
43
+ })
44
+ ]);
45
+ expect(webhookResult.status === "fulfilled" || directResult.status === "fulfilled").toBe(true);
46
+ const finalTransaction = await waitForCheckoutStatus(request, token, transactionId, "completed", {
47
+ attempts: 30,
48
+ intervalMs: 200
49
+ });
50
+ expect(finalTransaction.status).toBe("completed");
51
+ const updatedLinkResponse = await apiRequest(request, "GET", `/api/checkout/links/${encodeURIComponent(link.id)}`, { token });
52
+ const updatedLinkBody = await readJsonSafe(updatedLinkResponse);
53
+ expect(updatedLinkBody?.link?.completionCount).toBe(1);
54
+ const storedTransaction = await readCheckoutTransaction(request, token, transactionId);
55
+ expect(storedTransaction.status).toBe("completed");
56
+ expect(storedTransaction.gatewayTransactionId).toBeTruthy();
57
+ } finally {
58
+ await deleteCheckoutEntityIfExists(request, token, "links", linkId);
59
+ }
60
+ });
61
+ });
62
+ //# sourceMappingURL=TC-CHKT-043-concurrent-status-update.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.ts"],
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken, apiRequest } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport { readJsonSafe } from '@open-mercato/core/modules/core/__integration__/helpers/generalFixtures'\nimport {\n createFixedTemplateInput,\n createLinkFixture,\n deleteCheckoutEntityIfExists,\n findGatewayTransactionIdForCheckout,\n readCheckoutTransaction,\n sendMockGatewayWebhook,\n submitPayLink,\n waitForCheckoutStatus,\n} from './helpers/fixtures'\n\n/**\n * TC-CHKT-043: Two-contender concurrency test for updateTransactionStatus.\n *\n * Verifies that the compare-and-swap (CAS) in updateTransactionStatusCommand\n * serializes two concurrent writers correctly:\n *\n * - Exactly one of two concurrent updateStatus calls wins.\n * - The loser receives a 409 (concurrent_status_update).\n * - The persisted terminal status stays 'completed'.\n * - CheckoutLink.completionCount is incremented exactly once.\n *\n * This test exercises the race described in #4700 using a real database,\n * satisfying .ai/review-checklist.md \u00A720 (concurrency-sensitive commands\n * require an executable two-contender test).\n */\ntest.describe('TC-CHKT-043 (concurrency): Two-contender race \u2014 only one updateStatus call wins', () => {\n test('concurrent completed + processing status updates: one wins, one gets 409, completionCount incremented once', async ({ request }) => {\n let token: string | null = null\n let linkId: string | null = null\n\n try {\n token = await getAuthToken(request)\n\n // Create an active link that accepts a single payment.\n const link = await createLinkFixture(request, token, {\n ...createFixedTemplateInput({ status: 'active', collectCustomerDetails: false }),\n })\n linkId = link.id\n\n // Submit a checkout to create a transaction and a gateway session.\n const submitResponse = await submitPayLink(request, link.slug, {\n customerData: {},\n acceptedLegalConsents: {},\n amount: 49.99,\n })\n expect(submitResponse.status(), `Submit failed: ${submitResponse.status()}`).toBe(201)\n const submitBody = await submitResponse.json() as { transactionId: string }\n const transactionId = submitBody.transactionId\n expect(transactionId).toBeTruthy()\n\n // Find the mock-gateway session ID so we can fire the captured webhook.\n const gatewayTransactionId = await findGatewayTransactionIdForCheckout(request, token, transactionId)\n const gatewayTransaction = await apiRequest(request, 'GET', `/api/payment_gateways/transactions/${encodeURIComponent(gatewayTransactionId)}`, { token })\n const gtBody = await readJsonSafe<{ transaction?: { providerSessionId?: string | null } }>(gatewayTransaction)\n const providerSessionId = gtBody?.transaction?.providerSessionId\n expect(providerSessionId, 'providerSessionId is required for webhook simulation').toBeTruthy()\n\n // --- Two-contender race ---\n // Simultaneously fire:\n // (A) The gateway \"captured\" webhook ? tries to set status = 'completed'.\n // (B) A direct updateStatus API call ? tries to set status = 'processing'\n // (simulates the submit route's late write losing the race).\n //\n // Both target the same transaction. Exactly one must succeed; the other\n // must receive a 409 so no lost-update occurs.\n const [webhookResult, directResult] = await Promise.allSettled([\n sendMockGatewayWebhook(request, token!, providerSessionId!, 'captured', 49.99),\n apiRequest(request, 'POST', `/api/checkout/transactions/${encodeURIComponent(transactionId)}/status`, {\n token,\n data: { status: 'processing', paymentStatus: 'processing' },\n }),\n ])\n\n // At least the webhook path should have been attempted. The direct call\n // may receive 409 (CAS missed) or 422 (invalid transition) depending on\n // which writer committed first \u2014 both are acceptable losing outcomes.\n // The important invariant is the final persisted state.\n expect(webhookResult.status === 'fulfilled' || directResult.status === 'fulfilled').toBe(true)\n\n // Wait for the transaction to reach a terminal state (the webhook sets it\n // to 'completed'; the submit route's 'processing' write must not win).\n const finalTransaction = await waitForCheckoutStatus(request, token!, transactionId, 'completed', {\n attempts: 30,\n intervalMs: 200,\n })\n\n // --- Core assertions ---\n\n // 1. The persisted status must be 'completed' \u2014 not 'processing'.\n expect(finalTransaction.status).toBe('completed')\n\n // 2. completionCount must be exactly 1 \u2014 the terminal state must have\n // been applied exactly once, not zero or twice.\n const updatedLinkResponse = await apiRequest(request, 'GET', `/api/checkout/links/${encodeURIComponent(link.id)}`, { token })\n const updatedLinkBody = await readJsonSafe<{ link?: { completionCount?: number } }>(updatedLinkResponse)\n expect(updatedLinkBody?.link?.completionCount).toBe(1)\n\n // 3. The stored transaction must have a gatewayTransactionId from the webhook.\n const storedTransaction = await readCheckoutTransaction(request, token!, transactionId)\n expect(storedTransaction.status).toBe('completed')\n expect(storedTransaction.gatewayTransactionId).toBeTruthy()\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n})\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,cAAc,kBAAkB;AACzC,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAiBP,KAAK,SAAS,wFAAmF,MAAM;AACrG,OAAK,8GAA8G,OAAO,EAAE,QAAQ,MAAM;AACxI,QAAI,QAAuB;AAC3B,QAAI,SAAwB;AAE5B,QAAI;AACF,cAAQ,MAAM,aAAa,OAAO;AAGlC,YAAM,OAAO,MAAM,kBAAkB,SAAS,OAAO;AAAA,QACnD,GAAG,yBAAyB,EAAE,QAAQ,UAAU,wBAAwB,MAAM,CAAC;AAAA,MACjF,CAAC;AACD,eAAS,KAAK;AAGd,YAAM,iBAAiB,MAAM,cAAc,SAAS,KAAK,MAAM;AAAA,QAC7D,cAAc,CAAC;AAAA,QACf,uBAAuB,CAAC;AAAA,QACxB,QAAQ;AAAA,MACV,CAAC;AACD,aAAO,eAAe,OAAO,GAAG,kBAAkB,eAAe,OAAO,CAAC,EAAE,EAAE,KAAK,GAAG;AACrF,YAAM,aAAa,MAAM,eAAe,KAAK;AAC7C,YAAM,gBAAgB,WAAW;AACjC,aAAO,aAAa,EAAE,WAAW;AAGjC,YAAM,uBAAuB,MAAM,oCAAoC,SAAS,OAAO,aAAa;AACpG,YAAM,qBAAqB,MAAM,WAAW,SAAS,OAAO,sCAAsC,mBAAmB,oBAAoB,CAAC,IAAI,EAAE,MAAM,CAAC;AACvJ,YAAM,SAAS,MAAM,aAAsE,kBAAkB;AAC7G,YAAM,oBAAoB,QAAQ,aAAa;AAC/C,aAAO,mBAAmB,sDAAsD,EAAE,WAAW;AAU7F,YAAM,CAAC,eAAe,YAAY,IAAI,MAAM,QAAQ,WAAW;AAAA,QAC7D,uBAAuB,SAAS,OAAQ,mBAAoB,YAAY,KAAK;AAAA,QAC7E,WAAW,SAAS,QAAQ,8BAA8B,mBAAmB,aAAa,CAAC,WAAW;AAAA,UACpG;AAAA,UACA,MAAM,EAAE,QAAQ,cAAc,eAAe,aAAa;AAAA,QAC5D,CAAC;AAAA,MACH,CAAC;AAMD,aAAO,cAAc,WAAW,eAAe,aAAa,WAAW,WAAW,EAAE,KAAK,IAAI;AAI7F,YAAM,mBAAmB,MAAM,sBAAsB,SAAS,OAAQ,eAAe,aAAa;AAAA,QAChG,UAAU;AAAA,QACV,YAAY;AAAA,MACd,CAAC;AAKD,aAAO,iBAAiB,MAAM,EAAE,KAAK,WAAW;AAIhD,YAAM,sBAAsB,MAAM,WAAW,SAAS,OAAO,uBAAuB,mBAAmB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC;AAC5H,YAAM,kBAAkB,MAAM,aAAsD,mBAAmB;AACvG,aAAO,iBAAiB,MAAM,eAAe,EAAE,KAAK,CAAC;AAGrD,YAAM,oBAAoB,MAAM,wBAAwB,SAAS,OAAQ,aAAa;AACtF,aAAO,kBAAkB,MAAM,EAAE,KAAK,WAAW;AACjD,aAAO,kBAAkB,oBAAoB,EAAE,WAAW;AAAA,IAC5D,UAAE;AACA,YAAM,6BAA6B,SAAS,OAAO,SAAS,MAAM;AAAA,IACpE;AAAA,EACF,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
3
- import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
3
+ import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
4
4
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
5
5
  import { checkRateLimit } from "@open-mercato/shared/lib/ratelimit/helpers";
6
6
  import { GatewayTransaction } from "@open-mercato/core/modules/payment_gateways/data/entities";
@@ -351,8 +351,9 @@ async function POST(req, { params }) {
351
351
  const rendererKey = typeof link.gatewaySettings?.rendererKey === "string" && link.gatewaySettings.rendererKey.trim().length > 0 ? link.gatewaySettings.rendererKey.trim() : void 0;
352
352
  const rendererSettings = link.gatewaySettings?.rendererSettings && typeof link.gatewaySettings.rendererSettings === "object" && !Array.isArray(link.gatewaySettings.rendererSettings) ? link.gatewaySettings.rendererSettings : void 0;
353
353
  const presentationMode = link.gatewaySettings?.presentationMode === "embedded" || link.gatewaySettings?.presentationMode === "redirect" || link.gatewaySettings?.presentationMode === "auto" ? link.gatewaySettings.presentationMode : void 0;
354
+ let sessionResult;
354
355
  try {
355
- const sessionResult = await paymentGatewayService.createPaymentSession({
356
+ sessionResult = await paymentGatewayService.createPaymentSession({
356
357
  providerKey: link.gatewayProviderKey,
357
358
  paymentId: transactionId,
358
359
  idempotencyKey,
@@ -374,17 +375,6 @@ async function POST(req, { params }) {
374
375
  organizationId: link.organizationId,
375
376
  tenantId: link.tenantId
376
377
  });
377
- await commandBus.execute("checkout.transaction.updateStatus", {
378
- input: {
379
- id: transaction.id,
380
- status: mapGatewayStatusToCheckoutStatus(sessionResult.transaction.unifiedStatus),
381
- paymentStatus: sessionResult.transaction.unifiedStatus,
382
- gatewayTransactionId: sessionResult.transaction.id,
383
- organizationId: link.organizationId,
384
- tenantId: link.tenantId
385
- },
386
- ctx
387
- });
388
378
  } catch (error) {
389
379
  await commandBus.execute("checkout.transaction.updateStatus", {
390
380
  input: {
@@ -404,6 +394,29 @@ async function POST(req, { params }) {
404
394
  });
405
395
  throw new CrudHttpError(502, { error: "checkout.payPage.errors.sessionStart" });
406
396
  }
397
+ let statusConflict = false;
398
+ try {
399
+ await commandBus.execute("checkout.transaction.updateStatus", {
400
+ input: {
401
+ id: transaction.id,
402
+ status: mapGatewayStatusToCheckoutStatus(sessionResult.transaction.unifiedStatus),
403
+ paymentStatus: sessionResult.transaction.unifiedStatus,
404
+ gatewayTransactionId: sessionResult.transaction.id,
405
+ organizationId: link.organizationId,
406
+ tenantId: link.tenantId
407
+ },
408
+ ctx
409
+ });
410
+ } catch (error) {
411
+ const isConflict = isCrudHttpError(error) && error.status === 409 && (error.body?.code === "concurrent_status_update" || error.body?.code === "invalid_status_transition");
412
+ if (!isConflict) throw error;
413
+ logger.info("Transaction status already advanced by concurrent writer \u2014 webhook won the race", {
414
+ linkId: link.id,
415
+ transactionId: transaction.id,
416
+ conflictCode: error.body?.code
417
+ });
418
+ statusConflict = true;
419
+ }
407
420
  const refreshedTransaction = await findOneWithDecryption(em, CheckoutTransaction, {
408
421
  id: transaction.id,
409
422
  organizationId: link.organizationId,
@@ -412,21 +425,23 @@ async function POST(req, { params }) {
412
425
  if (!refreshedTransaction) {
413
426
  throw new CrudHttpError(404, { error: "Transaction not found" });
414
427
  }
415
- await emitCheckoutEvent("checkout.transaction.sessionStarted", {
416
- transactionId: refreshedTransaction.id,
417
- linkId: refreshedTransaction.linkId,
418
- templateId: link.templateId ?? null,
419
- slug: link.slug,
420
- status: refreshedTransaction.status,
421
- paymentStatus: refreshedTransaction.paymentStatus ?? null,
422
- amount: Number(refreshedTransaction.amount),
423
- currency: refreshedTransaction.currencyCode,
424
- gatewayProvider: link.gatewayProviderKey,
425
- gatewayTransactionId: refreshedTransaction.gatewayTransactionId ?? null,
426
- occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
427
- tenantId: link.tenantId,
428
- organizationId: link.organizationId
429
- }).catch(() => void 0);
428
+ if (!statusConflict) {
429
+ await emitCheckoutEvent("checkout.transaction.sessionStarted", {
430
+ transactionId: refreshedTransaction.id,
431
+ linkId: refreshedTransaction.linkId,
432
+ templateId: link.templateId ?? null,
433
+ slug: link.slug,
434
+ status: refreshedTransaction.status,
435
+ paymentStatus: refreshedTransaction.paymentStatus ?? null,
436
+ amount: Number(refreshedTransaction.amount),
437
+ currency: refreshedTransaction.currencyCode,
438
+ gatewayProvider: link.gatewayProviderKey,
439
+ gatewayTransactionId: refreshedTransaction.gatewayTransactionId ?? null,
440
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
441
+ tenantId: link.tenantId,
442
+ organizationId: link.organizationId
443
+ }).catch(() => void 0);
444
+ }
430
445
  return NextResponse.json(
431
446
  await buildSubmitResponse(req, em, link, refreshedTransaction, link.gatewayProviderKey),
432
447
  { status: 201 }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/checkout/api/pay/%5Bslug%5D/submit/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport { checkRateLimit } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { PaymentGatewayClientSession } from '@open-mercato/shared/modules/payment_gateways/types'\nimport type { PaymentGatewayService } from '@open-mercato/core/modules/payment_gateways/lib/gateway-service'\nimport { GatewayTransaction } from '@open-mercato/core/modules/payment_gateways/data/entities'\nimport { CheckoutLink, CheckoutTransaction } from '../../../../data/entities'\nimport { publicSubmitSchema } from '../../../../data/validators'\nimport { handleCheckoutRouteError, requireCheckoutPasswordSession } from '../../../helpers'\nimport { emitCheckoutEvent } from '../../../../events'\nimport {\n buildConsentProof,\n isCheckoutLinkPublic,\n mapGatewayStatusToCheckoutStatus,\n resolveSubmittedAmount,\n validateDescriptorCurrencies,\n} from '../../../../lib/utils'\nimport { validateCheckoutCustomerData } from '../../../../lib/customerDataValidation'\nimport { buildCheckoutRateLimitKey, checkoutSubmitRateLimitConfig } from '../../../../lib/rateLimiter'\nimport { checkoutTag } from '../../../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('checkout')\n\ntype CachedSubmitResponse = {\n transactionId: string\n redirectUrl?: string | null\n paymentSession?: (PaymentGatewayClientSession & {\n providerKey: string | null\n gatewayTransactionId: string\n }) | null\n}\n\ntype CheckoutLegalDocumentRequirement = {\n required?: boolean\n}\n\ntype CheckoutCustomerFieldRequirement = {\n key: string\n required?: boolean\n}\n\nfunction normalizePort(url: URL): string {\n if (url.port) return url.port\n return url.protocol === 'https:' ? '443' : '80'\n}\n\nfunction normalizeConfiguredOrigin(candidate: string): string | null {\n try {\n return new URL(candidate).origin\n } catch {\n return null\n }\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === '127.0.0.1' || hostname === 'localhost'\n}\n\nfunction splitHeaderValues(value: string | null): string[] {\n if (!value) return []\n return value\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n}\n\nfunction isAllowedRequestOrigin(submittedOrigin: string, allowedOrigin: string): boolean {\n if (submittedOrigin === allowedOrigin) return true\n try {\n const submittedUrl = new URL(submittedOrigin)\n const allowedUrl = new URL(allowedOrigin)\n return submittedUrl.protocol === allowedUrl.protocol\n && normalizePort(submittedUrl) === normalizePort(allowedUrl)\n && isLoopbackHostname(submittedUrl.hostname)\n && isLoopbackHostname(allowedUrl.hostname)\n } catch {\n return false\n }\n}\n\nconst CONFIGURED_ALLOWED_ORIGINS: string[] = (process.env.CHECKOUT_ALLOWED_ORIGINS ?? '')\n .split(',')\n .map((origin) => origin.trim())\n .filter((origin) => origin.length > 0)\n\nfunction addAllowedOrigin(allowedOrigins: Set<string>, candidate: string) {\n const normalized = normalizeConfiguredOrigin(candidate)\n if (!normalized) return\n allowedOrigins.add(normalized)\n try {\n const parsed = new URL(normalized)\n if (!isLoopbackHostname(parsed.hostname)) return\n for (const hostname of ['127.0.0.1', 'localhost']) {\n for (const protocol of ['http:', 'https:']) {\n const variant = new URL(normalized)\n variant.hostname = hostname\n variant.protocol = protocol\n allowedOrigins.add(variant.origin)\n }\n }\n } catch {\n // Ignore invalid variants.\n }\n}\n\nfunction collectConfiguredOrigins(): string[] {\n const origins = [...CONFIGURED_ALLOWED_ORIGINS]\n for (const origin of [process.env.APP_URL, process.env.NEXT_PUBLIC_APP_URL]) {\n if (origin) origins.push(origin)\n }\n return origins\n}\n\n// Allowed origins are built ONLY from server-configured values. The inbound\n// request URL, Host and X-Forwarded-Host headers are attacker-controllable on\n// deployments that do not normalize them at the edge, so they must never seed\n// the allowlist (otherwise a spoofed Host self-passes the origin check).\nfunction buildAllowedOrigins(): string[] {\n const allowedOrigins = new Set<string>()\n for (const origin of collectConfiguredOrigins()) {\n addAllowedOrigin(allowedOrigins, origin)\n }\n return Array.from(allowedOrigins)\n}\n\n// Resolve the origin used to build gateway-bound success/cancel/return URLs.\n// Always prefer a server-pinned configured origin; only fall back to the request\n// origin on loopback (local dev/test), where the Host cannot be spoofed by a\n// remote attacker. A non-loopback request with no configured origin is a\n// misconfiguration and is rejected rather than emitting an attacker-controllable URL.\nfunction resolveServerOrigin(req: Request): string {\n for (const candidate of collectConfiguredOrigins()) {\n const normalized = normalizeConfiguredOrigin(candidate)\n if (normalized) return normalized\n }\n const requestUrl = new URL(req.url)\n if (isLoopbackHostname(requestUrl.hostname)) return requestUrl.origin\n throw new CrudHttpError(500, { error: 'Checkout origin is not configured' })\n}\n\n// Normalize a host value (bare host, host:port, or full origin) to its authority\n// (`hostname` plus any non-default port), lower-cased. Protocol is intentionally\n// dropped: the gateway-bound URLs are already pinned to the configured origin, so\n// the host guard below only needs to reject foreign hosts \u2014 not enforce protocol\n// or internal-upstream-host parity, which would 403 legitimate proxied requests.\nfunction hostAuthority(value: string): string | null {\n const candidate = value.includes('://') ? value : `https://${value}`\n try {\n const url = new URL(candidate)\n if (!url.hostname) return null\n const port = url.port && url.port !== '80' && url.port !== '443' ? `:${url.port}` : ''\n return `${url.hostname.toLowerCase()}${port}`\n } catch {\n return null\n }\n}\n\nfunction allowedHostAuthorities(allowedOrigins: string[]): Set<string> {\n const authorities = new Set<string>()\n for (const origin of allowedOrigins) {\n const authority = hostAuthority(origin)\n if (authority) authorities.add(authority)\n }\n return authorities\n}\n\n// The externally-asserted host: the proxy-forwarded X-Forwarded-Host when present,\n// otherwise the Host header. The internal upstream Host (and protocol) are ignored\n// so a correctly-proxied request is not rejected for not matching the public origin.\nfunction requestHostAuthorities(req: Request): string[] {\n const forwarded = splitHeaderValues(req.headers.get('x-forwarded-host'))\n const hostValues = forwarded.length > 0 ? forwarded : splitHeaderValues(req.headers.get('host'))\n const authorities: string[] = []\n for (const value of hostValues) {\n const authority = hostAuthority(value)\n if (authority) authorities.push(authority)\n }\n return authorities\n}\n\nfunction isIdempotencyConflict(error: unknown): boolean {\n const message = error instanceof Error ? error.message : ''\n return message.includes('checkout_transactions_organization_id_tenant_id_link_idempotency_key_index')\n || message.includes('checkout_transactions_organization_id_tenant_id_link_idempotency_key_unique')\n || message.includes('duplicate key value')\n}\n\nfunction readClientSession(\n metadata: Record<string, unknown> | null | undefined,\n): PaymentGatewayClientSession | null {\n const candidate = metadata?.clientSession\n if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return null\n const type = (candidate as { type?: unknown }).type\n if (type === 'redirect') {\n const redirectUrl = (candidate as { redirectUrl?: unknown }).redirectUrl\n if (typeof redirectUrl !== 'string' || redirectUrl.trim().length === 0) return null\n const target = (candidate as { target?: unknown }).target\n return {\n type: 'redirect',\n redirectUrl,\n target: target === 'top' ? 'top' : 'self',\n }\n }\n if (type !== 'embedded') return null\n if (typeof (candidate as { rendererKey?: unknown }).rendererKey !== 'string') return null\n const payload = (candidate as { payload?: unknown }).payload\n const settings = (candidate as { settings?: unknown }).settings\n return {\n type: 'embedded',\n rendererKey: (candidate as { rendererKey: string }).rendererKey,\n payload: payload && typeof payload === 'object' && !Array.isArray(payload)\n ? payload as Record<string, unknown>\n : undefined,\n settings: settings && typeof settings === 'object' && !Array.isArray(settings)\n ? settings as Record<string, unknown>\n : undefined,\n }\n}\n\nasync function buildSubmitResponse(\n req: Request,\n em: EntityManager,\n link: CheckoutLink,\n transaction: CheckoutTransaction,\n providerKey: string | null | undefined,\n): Promise<CachedSubmitResponse> {\n const gatewayTransaction = transaction.gatewayTransactionId\n ? await em.findOne(GatewayTransaction, {\n id: transaction.gatewayTransactionId,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n deletedAt: null,\n })\n : null\n const serverOrigin = resolveServerOrigin(req)\n const clientSession = gatewayTransaction\n ? readClientSession(gatewayTransaction.gatewayMetadata)\n : null\n const paymentSession = (clientSession && gatewayTransaction)\n ? {\n ...clientSession,\n ...(clientSession.type === 'embedded'\n ? {\n payload: {\n ...(clientSession.payload ?? {}),\n returnUrl: `${serverOrigin}/pay/${encodeURIComponent(link.slug)}/success/${encodeURIComponent(transaction.id)}`,\n cancelUrl: `${serverOrigin}/pay/${encodeURIComponent(link.slug)}/cancel/${encodeURIComponent(transaction.id)}`,\n },\n }\n : {}),\n providerKey: providerKey ?? gatewayTransaction.providerKey ?? null,\n gatewayTransactionId: gatewayTransaction.id,\n }\n : null\n return {\n transactionId: transaction.id,\n redirectUrl: gatewayTransaction?.redirectUrl ?? null,\n paymentSession,\n }\n}\n\nexport const metadata = {\n path: '/checkout/pay/[slug]/submit',\n POST: { requireAuth: false },\n}\n\nexport async function POST(req: Request, { params }: { params: Promise<{ slug: string }> | { slug: string } }) {\n try {\n const container = await createRequestContainer()\n try {\n const rateLimiter = container.resolve('rateLimiterService') as RateLimiterService\n const key = buildCheckoutRateLimitKey(req, rateLimiter, 'checkout-submit')\n const rateLimitResponse = await checkRateLimit(rateLimiter, checkoutSubmitRateLimitConfig, key, 'Too many payment attempts. Please try again later.')\n if (rateLimitResponse) return rateLimitResponse\n } catch {\n // Rate limiting is fail-open\n }\n const resolvedParams = await params\n\n const allowedOrigins = buildAllowedOrigins()\n if (allowedOrigins.length > 0) {\n // Reject spoofed Host / X-Forwarded-Host before any business logic runs so\n // an attacker-controlled host can never flow into gateway-bound URLs. Hosts\n // are matched by authority (protocol-independent) so a correctly TLS-proxied\n // request \u2014 internal http, X-Forwarded-Proto https \u2014 is not rejected.\n const allowedAuthorities = allowedHostAuthorities(allowedOrigins)\n const hostAuthorities = requestHostAuthorities(req)\n if (\n hostAuthorities.length > 0\n && !hostAuthorities.every((authority) => allowedAuthorities.has(authority))\n ) {\n return NextResponse.json({ error: 'Invalid request host' }, { status: 403 })\n }\n\n const origin = req.headers.get('origin')\n const referer = req.headers.get('referer')\n if (origin || referer) {\n const submittedOrigin = origin ?? (referer ? new URL(referer).origin : null)\n if (submittedOrigin && !allowedOrigins.some((allowedOrigin) => isAllowedRequestOrigin(submittedOrigin, allowedOrigin))) {\n return NextResponse.json({ error: 'Invalid request origin' }, { status: 403 })\n }\n }\n }\n\n const idempotencyKey = req.headers.get('Idempotency-Key')?.trim()\n if (!idempotencyKey) {\n return NextResponse.json({ error: 'Idempotency-Key header is required' }, { status: 400 })\n }\n if (idempotencyKey.length < 16 || idempotencyKey.length > 128) {\n return NextResponse.json({ error: 'Idempotency-Key must be between 16 and 128 characters' }, { status: 400 })\n }\n\n const body = publicSubmitSchema.parse(await req.json().catch(() => ({})))\n const em = container.resolve('em')\n const commandBus = container.resolve('commandBus') as CommandBus\n const paymentGatewayService = container.resolve('paymentGatewayService') as PaymentGatewayService\n const link = await findOneWithDecryption(em, CheckoutLink, {\n slug: resolvedParams.slug,\n deletedAt: null,\n })\n if (!link) {\n return NextResponse.json({ error: 'Payment link not found' }, { status: 404 })\n }\n if (!isCheckoutLinkPublic(link.status)) {\n throw new CrudHttpError(422, { error: 'This payment link is not currently accepting payments' })\n }\n if (link.passwordHash) {\n requireCheckoutPasswordSession(req, link.slug, {\n linkId: link.id,\n sessionVersion: link.passwordHash,\n })\n }\n if (!link.gatewayProviderKey) {\n throw new CrudHttpError(422, { error: 'A payment gateway must be configured before this link can be used' })\n }\n const legalDocuments = link.legalDocuments && typeof link.legalDocuments === 'object'\n ? link.legalDocuments as Partial<Record<'terms' | 'privacyPolicy', CheckoutLegalDocumentRequirement>>\n : {}\n const legalConsentErrors: Record<string, string> = {}\n for (const key of ['terms', 'privacyPolicy'] as const) {\n const document = legalDocuments[key]\n if (document?.required === true && body.acceptedLegalConsents?.[key] !== true) {\n legalConsentErrors[`acceptedLegalConsents.${key}`] = 'checkout.payPage.validation.documentRequired'\n }\n }\n if (Object.keys(legalConsentErrors).length > 0) {\n throw new CrudHttpError(422, {\n error: 'checkout.payPage.validation.fixErrors',\n fieldErrors: legalConsentErrors,\n })\n }\n const collectedCustomerData = link.collectCustomerDetails === false ? {} : body.customerData\n const customerFields = Array.isArray(link.customerFieldsSchema)\n ? link.customerFieldsSchema as CheckoutCustomerFieldRequirement[]\n : []\n if (link.collectCustomerDetails !== false) {\n const customerFieldErrors = validateCheckoutCustomerData(customerFields, collectedCustomerData)\n if (Object.keys(customerFieldErrors).length > 0) {\n throw new CrudHttpError(422, {\n error: 'checkout.payPage.validation.fixErrors',\n fieldErrors: customerFieldErrors,\n })\n }\n }\n const resolvedAmount = resolveSubmittedAmount(link, body)\n const existingTransaction = await findOneWithDecryption(em, CheckoutTransaction, {\n linkId: link.id,\n idempotencyKey,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n }, undefined, { organizationId: link.organizationId, tenantId: link.tenantId })\n validateDescriptorCurrencies(link.gatewayProviderKey, [\n existingTransaction?.currencyCode ?? resolvedAmount.currencyCode,\n ])\n if (existingTransaction?.gatewayTransactionId) {\n return NextResponse.json(\n await buildSubmitResponse(req, em, link, existingTransaction, link.gatewayProviderKey),\n )\n }\n\n const transactionInput = {\n linkId: link.id,\n amount: resolvedAmount.amount,\n currencyCode: resolvedAmount.currencyCode,\n idempotencyKey,\n customerData: collectedCustomerData,\n firstName: typeof collectedCustomerData.firstName === 'string' ? collectedCustomerData.firstName : null,\n lastName: typeof collectedCustomerData.lastName === 'string' ? collectedCustomerData.lastName : null,\n email: typeof collectedCustomerData.email === 'string' ? collectedCustomerData.email : null,\n phone: typeof collectedCustomerData.phone === 'string' ? collectedCustomerData.phone : null,\n selectedPriceItemId: resolvedAmount.selectedPriceItemId,\n acceptedLegalConsents: buildConsentProof(link, body.acceptedLegalConsents),\n ipAddress: req.headers.get('x-forwarded-for') ?? null,\n userAgent: req.headers.get('user-agent') ?? null,\n tenantId: link.tenantId,\n organizationId: link.organizationId,\n }\n const ctx: CommandRuntimeContext = {\n container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: link.organizationId,\n organizationIds: [link.organizationId],\n request: req,\n }\n let transactionId = existingTransaction?.id ?? null\n if (!transactionId) {\n try {\n const created = await commandBus.execute<typeof transactionInput, { id: string }>('checkout.transaction.create', {\n input: transactionInput,\n ctx,\n })\n transactionId = created.result.id\n } catch (error) {\n if (!isIdempotencyConflict(error)) {\n throw error\n }\n const duplicated = await findOneWithDecryption(em, CheckoutTransaction, {\n linkId: link.id,\n idempotencyKey,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n }, undefined, { organizationId: link.organizationId, tenantId: link.tenantId })\n if (!duplicated) {\n throw error\n }\n transactionId = duplicated.id\n }\n }\n if (!transactionId) {\n throw new CrudHttpError(500, { error: 'Failed to initialize checkout transaction' })\n }\n const serverOrigin = resolveServerOrigin(req)\n const successUrl = `${serverOrigin}/pay/${encodeURIComponent(link.slug)}/success/${encodeURIComponent(transactionId)}`\n const cancelUrl = `${serverOrigin}/pay/${encodeURIComponent(link.slug)}/cancel/${encodeURIComponent(transactionId)}`\n const transaction = await findOneWithDecryption(em, CheckoutTransaction, {\n id: transactionId,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n }, undefined, { organizationId: link.organizationId, tenantId: link.tenantId })\n if (!transaction) {\n throw new CrudHttpError(404, { error: 'Transaction not found' })\n }\n const sessionAmount = Number(transaction.amount)\n if (!Number.isFinite(sessionAmount)) {\n throw new CrudHttpError(500, { error: 'Invalid checkout transaction amount' })\n }\n const sessionCurrencyCode = transaction.currencyCode\n if (!transaction.gatewayTransactionId) {\n const configuredPaymentTypes = Array.isArray(link.gatewaySettings?.paymentTypes)\n ? link.gatewaySettings.paymentTypes.filter(\n (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0,\n )\n : []\n const rendererKey = typeof link.gatewaySettings?.rendererKey === 'string' && link.gatewaySettings.rendererKey.trim().length > 0\n ? link.gatewaySettings.rendererKey.trim()\n : undefined\n const rendererSettings = link.gatewaySettings?.rendererSettings\n && typeof link.gatewaySettings.rendererSettings === 'object'\n && !Array.isArray(link.gatewaySettings.rendererSettings)\n ? link.gatewaySettings.rendererSettings as Record<string, unknown>\n : undefined\n const presentationMode = link.gatewaySettings?.presentationMode === 'embedded'\n || link.gatewaySettings?.presentationMode === 'redirect'\n || link.gatewaySettings?.presentationMode === 'auto'\n ? link.gatewaySettings.presentationMode\n : undefined\n try {\n const sessionResult = await paymentGatewayService.createPaymentSession({\n providerKey: link.gatewayProviderKey,\n paymentId: transactionId,\n idempotencyKey,\n amount: sessionAmount,\n currencyCode: sessionCurrencyCode,\n paymentTypes: configuredPaymentTypes.length > 0 ? configuredPaymentTypes : undefined,\n description: link.title ?? link.name,\n successUrl,\n cancelUrl,\n metadata: {\n checkoutLinkId: link.id,\n checkoutSlug: link.slug,\n },\n presentation: rendererKey || rendererSettings || presentationMode\n ? {\n ...(presentationMode ? { mode: presentationMode } : {}),\n ...(rendererKey ? { rendererKey } : {}),\n ...(rendererSettings ? { rendererSettings } : {}),\n }\n : undefined,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n })\n await commandBus.execute('checkout.transaction.updateStatus', {\n input: {\n id: transaction.id,\n status: mapGatewayStatusToCheckoutStatus(sessionResult.transaction.unifiedStatus),\n paymentStatus: sessionResult.transaction.unifiedStatus,\n gatewayTransactionId: sessionResult.transaction.id,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n },\n ctx,\n })\n } catch (error) {\n await commandBus.execute('checkout.transaction.updateStatus', {\n input: {\n id: transaction.id,\n status: 'failed',\n paymentStatus: transaction.paymentStatus ?? 'failed',\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n },\n ctx,\n }).catch(() => undefined)\n logger.error('Failed to create payment session', {\n linkId: link.id,\n transactionId: transaction.id,\n providerKey: link.gatewayProviderKey,\n err: error,\n })\n throw new CrudHttpError(502, { error: 'checkout.payPage.errors.sessionStart' })\n }\n const refreshedTransaction = await findOneWithDecryption(em, CheckoutTransaction, {\n id: transaction.id,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n }, undefined, { organizationId: link.organizationId, tenantId: link.tenantId })\n if (!refreshedTransaction) {\n throw new CrudHttpError(404, { error: 'Transaction not found' })\n }\n await emitCheckoutEvent('checkout.transaction.sessionStarted', {\n transactionId: refreshedTransaction.id,\n linkId: refreshedTransaction.linkId,\n templateId: link.templateId ?? null,\n slug: link.slug,\n status: refreshedTransaction.status,\n paymentStatus: refreshedTransaction.paymentStatus ?? null,\n amount: Number(refreshedTransaction.amount),\n currency: refreshedTransaction.currencyCode,\n gatewayProvider: link.gatewayProviderKey,\n gatewayTransactionId: refreshedTransaction.gatewayTransactionId ?? null,\n occurredAt: new Date().toISOString(),\n tenantId: link.tenantId,\n organizationId: link.organizationId,\n }).catch(() => undefined)\n return NextResponse.json(\n await buildSubmitResponse(req, em, link, refreshedTransaction, link.gatewayProviderKey),\n { status: 201 },\n )\n }\n return NextResponse.json(await buildSubmitResponse(req, em, link, transaction, link.gatewayProviderKey), { status: 201 })\n } catch (error) {\n return handleCheckoutRouteError(error)\n }\n}\n\nexport const openApi = {\n tags: [checkoutTag],\n}\n\nexport default POST\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport { checkRateLimit } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { PaymentGatewayClientSession } from '@open-mercato/shared/modules/payment_gateways/types'\nimport type { PaymentGatewayService } from '@open-mercato/core/modules/payment_gateways/lib/gateway-service'\nimport { GatewayTransaction } from '@open-mercato/core/modules/payment_gateways/data/entities'\nimport { CheckoutLink, CheckoutTransaction } from '../../../../data/entities'\nimport { publicSubmitSchema } from '../../../../data/validators'\nimport { handleCheckoutRouteError, requireCheckoutPasswordSession } from '../../../helpers'\nimport { emitCheckoutEvent } from '../../../../events'\nimport {\n buildConsentProof,\n isCheckoutLinkPublic,\n mapGatewayStatusToCheckoutStatus,\n resolveSubmittedAmount,\n validateDescriptorCurrencies,\n} from '../../../../lib/utils'\nimport { validateCheckoutCustomerData } from '../../../../lib/customerDataValidation'\nimport { buildCheckoutRateLimitKey, checkoutSubmitRateLimitConfig } from '../../../../lib/rateLimiter'\nimport { checkoutTag } from '../../../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('checkout')\n\ntype CachedSubmitResponse = {\n transactionId: string\n redirectUrl?: string | null\n paymentSession?: (PaymentGatewayClientSession & {\n providerKey: string | null\n gatewayTransactionId: string\n }) | null\n}\n\ntype CheckoutLegalDocumentRequirement = {\n required?: boolean\n}\n\ntype CheckoutCustomerFieldRequirement = {\n key: string\n required?: boolean\n}\n\nfunction normalizePort(url: URL): string {\n if (url.port) return url.port\n return url.protocol === 'https:' ? '443' : '80'\n}\n\nfunction normalizeConfiguredOrigin(candidate: string): string | null {\n try {\n return new URL(candidate).origin\n } catch {\n return null\n }\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === '127.0.0.1' || hostname === 'localhost'\n}\n\nfunction splitHeaderValues(value: string | null): string[] {\n if (!value) return []\n return value\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n}\n\nfunction isAllowedRequestOrigin(submittedOrigin: string, allowedOrigin: string): boolean {\n if (submittedOrigin === allowedOrigin) return true\n try {\n const submittedUrl = new URL(submittedOrigin)\n const allowedUrl = new URL(allowedOrigin)\n return submittedUrl.protocol === allowedUrl.protocol\n && normalizePort(submittedUrl) === normalizePort(allowedUrl)\n && isLoopbackHostname(submittedUrl.hostname)\n && isLoopbackHostname(allowedUrl.hostname)\n } catch {\n return false\n }\n}\n\nconst CONFIGURED_ALLOWED_ORIGINS: string[] = (process.env.CHECKOUT_ALLOWED_ORIGINS ?? '')\n .split(',')\n .map((origin) => origin.trim())\n .filter((origin) => origin.length > 0)\n\nfunction addAllowedOrigin(allowedOrigins: Set<string>, candidate: string) {\n const normalized = normalizeConfiguredOrigin(candidate)\n if (!normalized) return\n allowedOrigins.add(normalized)\n try {\n const parsed = new URL(normalized)\n if (!isLoopbackHostname(parsed.hostname)) return\n for (const hostname of ['127.0.0.1', 'localhost']) {\n for (const protocol of ['http:', 'https:']) {\n const variant = new URL(normalized)\n variant.hostname = hostname\n variant.protocol = protocol\n allowedOrigins.add(variant.origin)\n }\n }\n } catch {\n // Ignore invalid variants.\n }\n}\n\nfunction collectConfiguredOrigins(): string[] {\n const origins = [...CONFIGURED_ALLOWED_ORIGINS]\n for (const origin of [process.env.APP_URL, process.env.NEXT_PUBLIC_APP_URL]) {\n if (origin) origins.push(origin)\n }\n return origins\n}\n\n// Allowed origins are built ONLY from server-configured values. The inbound\n// request URL, Host and X-Forwarded-Host headers are attacker-controllable on\n// deployments that do not normalize them at the edge, so they must never seed\n// the allowlist (otherwise a spoofed Host self-passes the origin check).\nfunction buildAllowedOrigins(): string[] {\n const allowedOrigins = new Set<string>()\n for (const origin of collectConfiguredOrigins()) {\n addAllowedOrigin(allowedOrigins, origin)\n }\n return Array.from(allowedOrigins)\n}\n\n// Resolve the origin used to build gateway-bound success/cancel/return URLs.\n// Always prefer a server-pinned configured origin; only fall back to the request\n// origin on loopback (local dev/test), where the Host cannot be spoofed by a\n// remote attacker. A non-loopback request with no configured origin is a\n// misconfiguration and is rejected rather than emitting an attacker-controllable URL.\nfunction resolveServerOrigin(req: Request): string {\n for (const candidate of collectConfiguredOrigins()) {\n const normalized = normalizeConfiguredOrigin(candidate)\n if (normalized) return normalized\n }\n const requestUrl = new URL(req.url)\n if (isLoopbackHostname(requestUrl.hostname)) return requestUrl.origin\n throw new CrudHttpError(500, { error: 'Checkout origin is not configured' })\n}\n\n// Normalize a host value (bare host, host:port, or full origin) to its authority\n// (`hostname` plus any non-default port), lower-cased. Protocol is intentionally\n// dropped: the gateway-bound URLs are already pinned to the configured origin, so\n// the host guard below only needs to reject foreign hosts \u2014 not enforce protocol\n// or internal-upstream-host parity, which would 403 legitimate proxied requests.\nfunction hostAuthority(value: string): string | null {\n const candidate = value.includes('://') ? value : `https://${value}`\n try {\n const url = new URL(candidate)\n if (!url.hostname) return null\n const port = url.port && url.port !== '80' && url.port !== '443' ? `:${url.port}` : ''\n return `${url.hostname.toLowerCase()}${port}`\n } catch {\n return null\n }\n}\n\nfunction allowedHostAuthorities(allowedOrigins: string[]): Set<string> {\n const authorities = new Set<string>()\n for (const origin of allowedOrigins) {\n const authority = hostAuthority(origin)\n if (authority) authorities.add(authority)\n }\n return authorities\n}\n\n// The externally-asserted host: the proxy-forwarded X-Forwarded-Host when present,\n// otherwise the Host header. The internal upstream Host (and protocol) are ignored\n// so a correctly-proxied request is not rejected for not matching the public origin.\nfunction requestHostAuthorities(req: Request): string[] {\n const forwarded = splitHeaderValues(req.headers.get('x-forwarded-host'))\n const hostValues = forwarded.length > 0 ? forwarded : splitHeaderValues(req.headers.get('host'))\n const authorities: string[] = []\n for (const value of hostValues) {\n const authority = hostAuthority(value)\n if (authority) authorities.push(authority)\n }\n return authorities\n}\n\nfunction isIdempotencyConflict(error: unknown): boolean {\n const message = error instanceof Error ? error.message : ''\n return message.includes('checkout_transactions_organization_id_tenant_id_link_idempotency_key_index')\n || message.includes('checkout_transactions_organization_id_tenant_id_link_idempotency_key_unique')\n || message.includes('duplicate key value')\n}\n\nfunction readClientSession(\n metadata: Record<string, unknown> | null | undefined,\n): PaymentGatewayClientSession | null {\n const candidate = metadata?.clientSession\n if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return null\n const type = (candidate as { type?: unknown }).type\n if (type === 'redirect') {\n const redirectUrl = (candidate as { redirectUrl?: unknown }).redirectUrl\n if (typeof redirectUrl !== 'string' || redirectUrl.trim().length === 0) return null\n const target = (candidate as { target?: unknown }).target\n return {\n type: 'redirect',\n redirectUrl,\n target: target === 'top' ? 'top' : 'self',\n }\n }\n if (type !== 'embedded') return null\n if (typeof (candidate as { rendererKey?: unknown }).rendererKey !== 'string') return null\n const payload = (candidate as { payload?: unknown }).payload\n const settings = (candidate as { settings?: unknown }).settings\n return {\n type: 'embedded',\n rendererKey: (candidate as { rendererKey: string }).rendererKey,\n payload: payload && typeof payload === 'object' && !Array.isArray(payload)\n ? payload as Record<string, unknown>\n : undefined,\n settings: settings && typeof settings === 'object' && !Array.isArray(settings)\n ? settings as Record<string, unknown>\n : undefined,\n }\n}\n\nasync function buildSubmitResponse(\n req: Request,\n em: EntityManager,\n link: CheckoutLink,\n transaction: CheckoutTransaction,\n providerKey: string | null | undefined,\n): Promise<CachedSubmitResponse> {\n const gatewayTransaction = transaction.gatewayTransactionId\n ? await em.findOne(GatewayTransaction, {\n id: transaction.gatewayTransactionId,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n deletedAt: null,\n })\n : null\n const serverOrigin = resolveServerOrigin(req)\n const clientSession = gatewayTransaction\n ? readClientSession(gatewayTransaction.gatewayMetadata)\n : null\n const paymentSession = (clientSession && gatewayTransaction)\n ? {\n ...clientSession,\n ...(clientSession.type === 'embedded'\n ? {\n payload: {\n ...(clientSession.payload ?? {}),\n returnUrl: `${serverOrigin}/pay/${encodeURIComponent(link.slug)}/success/${encodeURIComponent(transaction.id)}`,\n cancelUrl: `${serverOrigin}/pay/${encodeURIComponent(link.slug)}/cancel/${encodeURIComponent(transaction.id)}`,\n },\n }\n : {}),\n providerKey: providerKey ?? gatewayTransaction.providerKey ?? null,\n gatewayTransactionId: gatewayTransaction.id,\n }\n : null\n return {\n transactionId: transaction.id,\n redirectUrl: gatewayTransaction?.redirectUrl ?? null,\n paymentSession,\n }\n}\n\nexport const metadata = {\n path: '/checkout/pay/[slug]/submit',\n POST: { requireAuth: false },\n}\n\nexport async function POST(req: Request, { params }: { params: Promise<{ slug: string }> | { slug: string } }) {\n try {\n const container = await createRequestContainer()\n try {\n const rateLimiter = container.resolve('rateLimiterService') as RateLimiterService\n const key = buildCheckoutRateLimitKey(req, rateLimiter, 'checkout-submit')\n const rateLimitResponse = await checkRateLimit(rateLimiter, checkoutSubmitRateLimitConfig, key, 'Too many payment attempts. Please try again later.')\n if (rateLimitResponse) return rateLimitResponse\n } catch {\n // Rate limiting is fail-open\n }\n const resolvedParams = await params\n\n const allowedOrigins = buildAllowedOrigins()\n if (allowedOrigins.length > 0) {\n // Reject spoofed Host / X-Forwarded-Host before any business logic runs so\n // an attacker-controlled host can never flow into gateway-bound URLs. Hosts\n // are matched by authority (protocol-independent) so a correctly TLS-proxied\n // request \u2014 internal http, X-Forwarded-Proto https \u2014 is not rejected.\n const allowedAuthorities = allowedHostAuthorities(allowedOrigins)\n const hostAuthorities = requestHostAuthorities(req)\n if (\n hostAuthorities.length > 0\n && !hostAuthorities.every((authority) => allowedAuthorities.has(authority))\n ) {\n return NextResponse.json({ error: 'Invalid request host' }, { status: 403 })\n }\n\n const origin = req.headers.get('origin')\n const referer = req.headers.get('referer')\n if (origin || referer) {\n const submittedOrigin = origin ?? (referer ? new URL(referer).origin : null)\n if (submittedOrigin && !allowedOrigins.some((allowedOrigin) => isAllowedRequestOrigin(submittedOrigin, allowedOrigin))) {\n return NextResponse.json({ error: 'Invalid request origin' }, { status: 403 })\n }\n }\n }\n\n const idempotencyKey = req.headers.get('Idempotency-Key')?.trim()\n if (!idempotencyKey) {\n return NextResponse.json({ error: 'Idempotency-Key header is required' }, { status: 400 })\n }\n if (idempotencyKey.length < 16 || idempotencyKey.length > 128) {\n return NextResponse.json({ error: 'Idempotency-Key must be between 16 and 128 characters' }, { status: 400 })\n }\n\n const body = publicSubmitSchema.parse(await req.json().catch(() => ({})))\n const em = container.resolve('em')\n const commandBus = container.resolve('commandBus') as CommandBus\n const paymentGatewayService = container.resolve('paymentGatewayService') as PaymentGatewayService\n const link = await findOneWithDecryption(em, CheckoutLink, {\n slug: resolvedParams.slug,\n deletedAt: null,\n })\n if (!link) {\n return NextResponse.json({ error: 'Payment link not found' }, { status: 404 })\n }\n if (!isCheckoutLinkPublic(link.status)) {\n throw new CrudHttpError(422, { error: 'This payment link is not currently accepting payments' })\n }\n if (link.passwordHash) {\n requireCheckoutPasswordSession(req, link.slug, {\n linkId: link.id,\n sessionVersion: link.passwordHash,\n })\n }\n if (!link.gatewayProviderKey) {\n throw new CrudHttpError(422, { error: 'A payment gateway must be configured before this link can be used' })\n }\n const legalDocuments = link.legalDocuments && typeof link.legalDocuments === 'object'\n ? link.legalDocuments as Partial<Record<'terms' | 'privacyPolicy', CheckoutLegalDocumentRequirement>>\n : {}\n const legalConsentErrors: Record<string, string> = {}\n for (const key of ['terms', 'privacyPolicy'] as const) {\n const document = legalDocuments[key]\n if (document?.required === true && body.acceptedLegalConsents?.[key] !== true) {\n legalConsentErrors[`acceptedLegalConsents.${key}`] = 'checkout.payPage.validation.documentRequired'\n }\n }\n if (Object.keys(legalConsentErrors).length > 0) {\n throw new CrudHttpError(422, {\n error: 'checkout.payPage.validation.fixErrors',\n fieldErrors: legalConsentErrors,\n })\n }\n const collectedCustomerData = link.collectCustomerDetails === false ? {} : body.customerData\n const customerFields = Array.isArray(link.customerFieldsSchema)\n ? link.customerFieldsSchema as CheckoutCustomerFieldRequirement[]\n : []\n if (link.collectCustomerDetails !== false) {\n const customerFieldErrors = validateCheckoutCustomerData(customerFields, collectedCustomerData)\n if (Object.keys(customerFieldErrors).length > 0) {\n throw new CrudHttpError(422, {\n error: 'checkout.payPage.validation.fixErrors',\n fieldErrors: customerFieldErrors,\n })\n }\n }\n const resolvedAmount = resolveSubmittedAmount(link, body)\n const existingTransaction = await findOneWithDecryption(em, CheckoutTransaction, {\n linkId: link.id,\n idempotencyKey,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n }, undefined, { organizationId: link.organizationId, tenantId: link.tenantId })\n validateDescriptorCurrencies(link.gatewayProviderKey, [\n existingTransaction?.currencyCode ?? resolvedAmount.currencyCode,\n ])\n if (existingTransaction?.gatewayTransactionId) {\n return NextResponse.json(\n await buildSubmitResponse(req, em, link, existingTransaction, link.gatewayProviderKey),\n )\n }\n\n const transactionInput = {\n linkId: link.id,\n amount: resolvedAmount.amount,\n currencyCode: resolvedAmount.currencyCode,\n idempotencyKey,\n customerData: collectedCustomerData,\n firstName: typeof collectedCustomerData.firstName === 'string' ? collectedCustomerData.firstName : null,\n lastName: typeof collectedCustomerData.lastName === 'string' ? collectedCustomerData.lastName : null,\n email: typeof collectedCustomerData.email === 'string' ? collectedCustomerData.email : null,\n phone: typeof collectedCustomerData.phone === 'string' ? collectedCustomerData.phone : null,\n selectedPriceItemId: resolvedAmount.selectedPriceItemId,\n acceptedLegalConsents: buildConsentProof(link, body.acceptedLegalConsents),\n ipAddress: req.headers.get('x-forwarded-for') ?? null,\n userAgent: req.headers.get('user-agent') ?? null,\n tenantId: link.tenantId,\n organizationId: link.organizationId,\n }\n const ctx: CommandRuntimeContext = {\n container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: link.organizationId,\n organizationIds: [link.organizationId],\n request: req,\n }\n let transactionId = existingTransaction?.id ?? null\n if (!transactionId) {\n try {\n const created = await commandBus.execute<typeof transactionInput, { id: string }>('checkout.transaction.create', {\n input: transactionInput,\n ctx,\n })\n transactionId = created.result.id\n } catch (error) {\n if (!isIdempotencyConflict(error)) {\n throw error\n }\n const duplicated = await findOneWithDecryption(em, CheckoutTransaction, {\n linkId: link.id,\n idempotencyKey,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n }, undefined, { organizationId: link.organizationId, tenantId: link.tenantId })\n if (!duplicated) {\n throw error\n }\n transactionId = duplicated.id\n }\n }\n if (!transactionId) {\n throw new CrudHttpError(500, { error: 'Failed to initialize checkout transaction' })\n }\n const serverOrigin = resolveServerOrigin(req)\n const successUrl = `${serverOrigin}/pay/${encodeURIComponent(link.slug)}/success/${encodeURIComponent(transactionId)}`\n const cancelUrl = `${serverOrigin}/pay/${encodeURIComponent(link.slug)}/cancel/${encodeURIComponent(transactionId)}`\n const transaction = await findOneWithDecryption(em, CheckoutTransaction, {\n id: transactionId,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n }, undefined, { organizationId: link.organizationId, tenantId: link.tenantId })\n if (!transaction) {\n throw new CrudHttpError(404, { error: 'Transaction not found' })\n }\n const sessionAmount = Number(transaction.amount)\n if (!Number.isFinite(sessionAmount)) {\n throw new CrudHttpError(500, { error: 'Invalid checkout transaction amount' })\n }\n const sessionCurrencyCode = transaction.currencyCode\n if (!transaction.gatewayTransactionId) {\n const configuredPaymentTypes = Array.isArray(link.gatewaySettings?.paymentTypes)\n ? link.gatewaySettings.paymentTypes.filter(\n (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0,\n )\n : []\n const rendererKey = typeof link.gatewaySettings?.rendererKey === 'string' && link.gatewaySettings.rendererKey.trim().length > 0\n ? link.gatewaySettings.rendererKey.trim()\n : undefined\n const rendererSettings = link.gatewaySettings?.rendererSettings\n && typeof link.gatewaySettings.rendererSettings === 'object'\n && !Array.isArray(link.gatewaySettings.rendererSettings)\n ? link.gatewaySettings.rendererSettings as Record<string, unknown>\n : undefined\n const presentationMode = link.gatewaySettings?.presentationMode === 'embedded'\n || link.gatewaySettings?.presentationMode === 'redirect'\n || link.gatewaySettings?.presentationMode === 'auto'\n ? link.gatewaySettings.presentationMode\n : undefined\n\n let sessionResult: Awaited<ReturnType<typeof paymentGatewayService.createPaymentSession>>\n try {\n sessionResult = await paymentGatewayService.createPaymentSession({\n providerKey: link.gatewayProviderKey,\n paymentId: transactionId,\n idempotencyKey,\n amount: sessionAmount,\n currencyCode: sessionCurrencyCode,\n paymentTypes: configuredPaymentTypes.length > 0 ? configuredPaymentTypes : undefined,\n description: link.title ?? link.name,\n successUrl,\n cancelUrl,\n metadata: {\n checkoutLinkId: link.id,\n checkoutSlug: link.slug,\n },\n presentation: rendererKey || rendererSettings || presentationMode\n ? {\n ...(presentationMode ? { mode: presentationMode } : {}),\n ...(rendererKey ? { rendererKey } : {}),\n ...(rendererSettings ? { rendererSettings } : {}),\n }\n : undefined,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n })\n } catch (error) {\n // The gateway itself failed \u2014 mark the transaction failed and surface 502.\n await commandBus.execute('checkout.transaction.updateStatus', {\n input: {\n id: transaction.id,\n status: 'failed',\n paymentStatus: transaction.paymentStatus ?? 'failed',\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n },\n ctx,\n }).catch(() => undefined)\n logger.error('Failed to create payment session', {\n linkId: link.id,\n transactionId: transaction.id,\n providerKey: link.gatewayProviderKey,\n err: error,\n })\n throw new CrudHttpError(502, { error: 'checkout.payPage.errors.sessionStart' })\n }\n\n // Update the transaction status to reflect the gateway response.\n // If the webhook already raced ahead and completed this transaction,\n // updateStatus will throw a 409 conflict \u2014 that is correct behavior and\n // means the payment succeeded, so we treat it as benign and fall through\n // to return the real, already-terminal transaction below.\n let statusConflict = false\n try {\n await commandBus.execute('checkout.transaction.updateStatus', {\n input: {\n id: transaction.id,\n status: mapGatewayStatusToCheckoutStatus(sessionResult.transaction.unifiedStatus),\n paymentStatus: sessionResult.transaction.unifiedStatus,\n gatewayTransactionId: sessionResult.transaction.id,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n },\n ctx,\n })\n } catch (error) {\n const isConflict = isCrudHttpError(error)\n && error.status === 409\n && (\n (error.body as Record<string, unknown> | undefined)?.code === 'concurrent_status_update'\n || (error.body as Record<string, unknown> | undefined)?.code === 'invalid_status_transition'\n )\n if (!isConflict) throw error\n logger.info('Transaction status already advanced by concurrent writer \u2014 webhook won the race', {\n linkId: link.id,\n transactionId: transaction.id,\n conflictCode: (error.body as Record<string, unknown> | undefined)?.code,\n })\n statusConflict = true\n }\n\n const refreshedTransaction = await findOneWithDecryption(em, CheckoutTransaction, {\n id: transaction.id,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n }, undefined, { organizationId: link.organizationId, tenantId: link.tenantId })\n if (!refreshedTransaction) {\n throw new CrudHttpError(404, { error: 'Transaction not found' })\n }\n if (!statusConflict) {\n await emitCheckoutEvent('checkout.transaction.sessionStarted', {\n transactionId: refreshedTransaction.id,\n linkId: refreshedTransaction.linkId,\n templateId: link.templateId ?? null,\n slug: link.slug,\n status: refreshedTransaction.status,\n paymentStatus: refreshedTransaction.paymentStatus ?? null,\n amount: Number(refreshedTransaction.amount),\n currency: refreshedTransaction.currencyCode,\n gatewayProvider: link.gatewayProviderKey,\n gatewayTransactionId: refreshedTransaction.gatewayTransactionId ?? null,\n occurredAt: new Date().toISOString(),\n tenantId: link.tenantId,\n organizationId: link.organizationId,\n }).catch(() => undefined)\n }\n return NextResponse.json(\n await buildSubmitResponse(req, em, link, refreshedTransaction, link.gatewayProviderKey),\n { status: 201 },\n )\n }\n\n return NextResponse.json(await buildSubmitResponse(req, em, link, transaction, link.gatewayProviderKey), { status: 201 })\n } catch (error) {\n return handleCheckoutRouteError(error)\n }\n}\n\nexport const openApi = {\n tags: [checkoutTag],\n}\n\nexport default POST\n"],
5
+ "mappings": "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",
6
6
  "names": ["metadata"]
7
7
  }
@@ -10,6 +10,7 @@ import {
10
10
  parseCheckoutInput,
11
11
  toMoneyString
12
12
  } from "../lib/utils.js";
13
+ import { assertValidCheckoutStatusTransition } from "../lib/transaction-status-machine.js";
13
14
  function resolveTransactionScope(input) {
14
15
  if (!input.organizationId || !input.tenantId) {
15
16
  throw new CrudHttpError(400, { error: "Transaction scope is required" });
@@ -145,10 +146,39 @@ const updateTransactionStatusCommand = {
145
146
  const nextStatus = parsed.status;
146
147
  const previousTerminal = isTerminalCheckoutStatus(previousStatus);
147
148
  const nextTerminal = isTerminalCheckoutStatus(nextStatus);
148
- transaction.status = nextStatus;
149
- transaction.paymentStatus = parsed.paymentStatus ?? transaction.paymentStatus ?? null;
150
- transaction.gatewayTransactionId = parsed.gatewayTransactionId ?? transaction.gatewayTransactionId ?? null;
151
- await tx.flush();
149
+ assertValidCheckoutStatusTransition(previousStatus, nextStatus);
150
+ const newPaymentStatus = parsed.paymentStatus ?? transaction.paymentStatus ?? null;
151
+ const newGatewayTransactionId = parsed.gatewayTransactionId ?? transaction.gatewayTransactionId ?? null;
152
+ const affected = await tx.nativeUpdate(
153
+ CheckoutTransaction,
154
+ {
155
+ id: parsed.id,
156
+ organizationId: scope.organizationId,
157
+ tenantId: scope.tenantId,
158
+ status: previousStatus
159
+ },
160
+ {
161
+ status: nextStatus,
162
+ paymentStatus: newPaymentStatus,
163
+ gatewayTransactionId: newGatewayTransactionId,
164
+ updatedAt: /* @__PURE__ */ new Date()
165
+ }
166
+ );
167
+ if (affected === 0) {
168
+ const actualTx = await tx.findOne(
169
+ CheckoutTransaction,
170
+ { id: parsed.id, organizationId: scope.organizationId, tenantId: scope.tenantId },
171
+ { fields: ["status"], refresh: true }
172
+ );
173
+ throw new CrudHttpError(409, {
174
+ error: `[internal] Transaction status was already updated by a concurrent process (expected "${previousStatus}", actual "${actualTx?.status ?? "unknown"}")`,
175
+ code: "concurrent_status_update",
176
+ expectedStatus: previousStatus,
177
+ currentStatus: actualTx?.status ?? "unknown",
178
+ requestedStatus: nextStatus
179
+ });
180
+ }
181
+ await tx.refresh(transaction);
152
182
  if (!previousTerminal && nextTerminal) {
153
183
  const { usageLimitReached } = applyTerminalTransactionState(link, nextStatus);
154
184
  await tx.flush();
@@ -158,7 +188,7 @@ const updateTransactionStatusCommand = {
158
188
  usageLimitReachedLinkSlug = link.slug;
159
189
  }
160
190
  }
161
- if (nextTerminal) {
191
+ if (nextTerminal && previousStatus !== nextStatus) {
162
192
  terminalEventPayload = {
163
193
  transactionId: transaction.id,
164
194
  linkId: transaction.linkId,
@@ -176,14 +206,16 @@ const updateTransactionStatusCommand = {
176
206
  };
177
207
  }
178
208
  });
179
- if (parsed.status === "completed") {
180
- await emitCheckoutEvent("checkout.transaction.completed", terminalEventPayload ?? {}).catch(() => void 0);
181
- } else if (parsed.status === "failed") {
182
- await emitCheckoutEvent("checkout.transaction.failed", terminalEventPayload ?? {}).catch(() => void 0);
183
- } else if (parsed.status === "cancelled") {
184
- await emitCheckoutEvent("checkout.transaction.cancelled", terminalEventPayload ?? {}).catch(() => void 0);
185
- } else if (parsed.status === "expired") {
186
- await emitCheckoutEvent("checkout.transaction.expired", terminalEventPayload ?? {}).catch(() => void 0);
209
+ if (terminalEventPayload !== null) {
210
+ if (parsed.status === "completed") {
211
+ await emitCheckoutEvent("checkout.transaction.completed", terminalEventPayload).catch(() => void 0);
212
+ } else if (parsed.status === "failed") {
213
+ await emitCheckoutEvent("checkout.transaction.failed", terminalEventPayload).catch(() => void 0);
214
+ } else if (parsed.status === "cancelled") {
215
+ await emitCheckoutEvent("checkout.transaction.cancelled", terminalEventPayload).catch(() => void 0);
216
+ } else if (parsed.status === "expired") {
217
+ await emitCheckoutEvent("checkout.transaction.expired", terminalEventPayload).catch(() => void 0);
218
+ }
187
219
  }
188
220
  if (emitUsageLimitReached && usageLimitReachedLinkId && usageLimitReachedLinkSlug) {
189
221
  await emitCheckoutEvent("checkout.link.usageLimitReached", {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/checkout/commands/transactions.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { CheckoutLink, CheckoutTransaction } from '../data/entities'\nimport { transactionCreateSchema, transactionUpdateStatusSchema } from '../data/validators'\nimport { emitCheckoutEvent } from '../events'\nimport {\n applyTerminalTransactionState,\n isTerminalCheckoutStatus,\n mapGatewayStatusToCheckoutStatus,\n parseCheckoutInput,\n toMoneyString,\n} from '../lib/utils'\n\nfunction resolveTransactionScope(input: { tenantId?: string | null; organizationId?: string | null }) {\n if (!input.organizationId || !input.tenantId) {\n throw new CrudHttpError(400, { error: 'Transaction scope is required' })\n }\n return {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n }\n}\n\ntype CheckoutTerminalEventPayload = {\n transactionId: string\n linkId: string\n templateId: string | null\n slug: string\n status: CheckoutTransaction['status']\n paymentStatus: string | null\n amount: number\n currency: string\n gatewayProvider: string | null\n gatewayTransactionId: string | null\n occurredAt: string\n tenantId: string\n organizationId: string\n}\n\nconst createTransactionCommand: CommandHandler<Record<string, unknown>, { id: string }> = {\n id: 'checkout.transaction.create',\n async execute(rawInput, ctx) {\n const { parsed } = parseCheckoutInput(rawInput, transactionCreateSchema.parse)\n const scope = resolveTransactionScope(parsed)\n const em = ctx.container.resolve('em') as EntityManager\n let lockedLinkId: string | null = null\n let lockedLinkSlug: string | null = null\n let lockedLinkTemplateId: string | null = null\n let lockedLinkGatewayProvider: string | null = null\n let shouldEmitLockedEvent = false\n const transaction = await em.transactional(async (tx) => {\n const currentLink = await tx.findOne(CheckoutLink, {\n id: parsed.linkId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n })\n if (!currentLink) {\n throw new CrudHttpError(404, { error: 'Payment link not found' })\n }\n if (currentLink.status !== 'active') {\n throw new CrudHttpError(422, { error: 'This payment link is not currently accepting payments' })\n }\n const reserved = await tx.getConnection().execute<Array<{ id: string }>>(\n `\n UPDATE checkout_links\n SET active_reservation_count = active_reservation_count + 1,\n is_locked = true,\n updated_at = now()\n WHERE id = ?\n AND organization_id = ?\n AND tenant_id = ?\n AND deleted_at IS NULL\n AND status = 'active'\n AND (\n max_completions IS NULL\n OR completion_count + active_reservation_count < max_completions\n )\n RETURNING id\n `,\n [parsed.linkId, scope.organizationId, scope.tenantId],\n )\n if (!reserved[0]?.id) {\n throw new CrudHttpError(422, { error: 'This payment link is no longer available' })\n }\n lockedLinkId = currentLink.id\n lockedLinkSlug = currentLink.slug\n lockedLinkTemplateId = currentLink.templateId ?? null\n lockedLinkGatewayProvider = currentLink.gatewayProviderKey ?? null\n shouldEmitLockedEvent = !currentLink.isLocked\n const transaction = tx.create(CheckoutTransaction, {\n ...parsed,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n amount: toMoneyString(parsed.amount) ?? '0.00',\n status: 'processing',\n })\n tx.persist(transaction)\n await tx.flush()\n return transaction\n })\n if (shouldEmitLockedEvent && lockedLinkId && lockedLinkSlug) {\n await emitCheckoutEvent('checkout.link.locked', {\n id: lockedLinkId,\n slug: lockedLinkSlug,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n }).catch(() => undefined)\n }\n await emitCheckoutEvent('checkout.transaction.created', {\n transactionId: transaction.id,\n linkId: transaction.linkId,\n status: transaction.status,\n amount: Number(transaction.amount),\n currency: transaction.currencyCode,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n }).catch(() => undefined)\n await emitCheckoutEvent('checkout.transaction.customerDataCaptured', {\n transactionId: transaction.id,\n linkId: transaction.linkId,\n templateId: lockedLinkTemplateId,\n slug: lockedLinkSlug,\n status: transaction.status,\n paymentStatus: transaction.paymentStatus ?? null,\n amount: Number(transaction.amount),\n currency: transaction.currencyCode,\n gatewayProvider: lockedLinkGatewayProvider,\n gatewayTransactionId: transaction.gatewayTransactionId ?? null,\n occurredAt: new Date().toISOString(),\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n customerDataCaptured: true,\n }).catch(() => undefined)\n return { id: transaction.id }\n },\n}\n\nconst updateTransactionStatusCommand: CommandHandler<Record<string, unknown>, { ok: true }> = {\n id: 'checkout.transaction.updateStatus',\n async execute(rawInput, ctx) {\n const { parsed } = parseCheckoutInput(rawInput, transactionUpdateStatusSchema.parse)\n const scope = resolveTransactionScope(parsed)\n const em = ctx.container.resolve('em') as EntityManager\n let terminalEventPayload: CheckoutTerminalEventPayload | null = null\n let emitUsageLimitReached = false\n let usageLimitReachedLinkId: string | null = null\n let usageLimitReachedLinkSlug: string | null = null\n await em.transactional(async (tx) => {\n const transaction = await tx.findOne(CheckoutTransaction, {\n id: parsed.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n if (!transaction) throw new CrudHttpError(404, { error: 'Transaction not found' })\n const link = await tx.findOne(CheckoutLink, {\n id: transaction.linkId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n })\n if (!link) throw new CrudHttpError(404, { error: 'Payment link not found' })\n\n const previousStatus = transaction.status\n const nextStatus = parsed.status\n const previousTerminal = isTerminalCheckoutStatus(previousStatus)\n const nextTerminal = isTerminalCheckoutStatus(nextStatus)\n\n transaction.status = nextStatus\n transaction.paymentStatus = parsed.paymentStatus ?? transaction.paymentStatus ?? null\n transaction.gatewayTransactionId = parsed.gatewayTransactionId ?? transaction.gatewayTransactionId ?? null\n await tx.flush()\n\n if (!previousTerminal && nextTerminal) {\n const { usageLimitReached } = applyTerminalTransactionState(link, nextStatus)\n await tx.flush()\n if (usageLimitReached) {\n emitUsageLimitReached = true\n usageLimitReachedLinkId = link.id\n usageLimitReachedLinkSlug = link.slug\n }\n }\n if (nextTerminal) {\n terminalEventPayload = {\n transactionId: transaction.id,\n linkId: transaction.linkId,\n templateId: link.templateId ?? null,\n slug: link.slug,\n status: transaction.status,\n paymentStatus: transaction.paymentStatus ?? null,\n amount: Number(transaction.amount),\n currency: transaction.currencyCode,\n gatewayProvider: link.gatewayProviderKey ?? null,\n gatewayTransactionId: transaction.gatewayTransactionId ?? null,\n occurredAt: new Date().toISOString(),\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n }\n }\n })\n if (parsed.status === 'completed') {\n await emitCheckoutEvent('checkout.transaction.completed', terminalEventPayload ?? {}).catch(() => undefined)\n } else if (parsed.status === 'failed') {\n await emitCheckoutEvent('checkout.transaction.failed', terminalEventPayload ?? {}).catch(() => undefined)\n } else if (parsed.status === 'cancelled') {\n await emitCheckoutEvent('checkout.transaction.cancelled', terminalEventPayload ?? {}).catch(() => undefined)\n } else if (parsed.status === 'expired') {\n await emitCheckoutEvent('checkout.transaction.expired', terminalEventPayload ?? {}).catch(() => undefined)\n }\n if (emitUsageLimitReached && usageLimitReachedLinkId && usageLimitReachedLinkSlug) {\n await emitCheckoutEvent('checkout.link.usageLimitReached', {\n id: usageLimitReachedLinkId,\n slug: usageLimitReachedLinkSlug,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n }).catch(() => undefined)\n }\n return { ok: true }\n },\n}\n\nregisterCommand(createTransactionCommand)\nregisterCommand(updateTransactionStatusCommand)\n\nexport function mapGatewayStatusForCommand(status: string | null | undefined) {\n return mapGatewayStatusToCheckoutStatus(status)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { CheckoutLink, CheckoutTransaction } from '../data/entities'\nimport { transactionCreateSchema, transactionUpdateStatusSchema } from '../data/validators'\nimport { emitCheckoutEvent } from '../events'\nimport {\n applyTerminalTransactionState,\n isTerminalCheckoutStatus,\n mapGatewayStatusToCheckoutStatus,\n parseCheckoutInput,\n toMoneyString,\n} from '../lib/utils'\nimport { assertValidCheckoutStatusTransition } from '../lib/transaction-status-machine'\n\nfunction resolveTransactionScope(input: { tenantId?: string | null; organizationId?: string | null }) {\n if (!input.organizationId || !input.tenantId) {\n throw new CrudHttpError(400, { error: 'Transaction scope is required' })\n }\n return {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n }\n}\n\ntype CheckoutTerminalEventPayload = {\n transactionId: string\n linkId: string\n templateId: string | null\n slug: string\n status: CheckoutTransaction['status']\n paymentStatus: string | null\n amount: number\n currency: string\n gatewayProvider: string | null\n gatewayTransactionId: string | null\n occurredAt: string\n tenantId: string\n organizationId: string\n}\n\nconst createTransactionCommand: CommandHandler<Record<string, unknown>, { id: string }> = {\n id: 'checkout.transaction.create',\n async execute(rawInput, ctx) {\n const { parsed } = parseCheckoutInput(rawInput, transactionCreateSchema.parse)\n const scope = resolveTransactionScope(parsed)\n const em = ctx.container.resolve('em') as EntityManager\n let lockedLinkId: string | null = null\n let lockedLinkSlug: string | null = null\n let lockedLinkTemplateId: string | null = null\n let lockedLinkGatewayProvider: string | null = null\n let shouldEmitLockedEvent = false\n const transaction = await em.transactional(async (tx) => {\n const currentLink = await tx.findOne(CheckoutLink, {\n id: parsed.linkId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n })\n if (!currentLink) {\n throw new CrudHttpError(404, { error: 'Payment link not found' })\n }\n if (currentLink.status !== 'active') {\n throw new CrudHttpError(422, { error: 'This payment link is not currently accepting payments' })\n }\n const reserved = await tx.getConnection().execute<Array<{ id: string }>>(\n `\n UPDATE checkout_links\n SET active_reservation_count = active_reservation_count + 1,\n is_locked = true,\n updated_at = now()\n WHERE id = ?\n AND organization_id = ?\n AND tenant_id = ?\n AND deleted_at IS NULL\n AND status = 'active'\n AND (\n max_completions IS NULL\n OR completion_count + active_reservation_count < max_completions\n )\n RETURNING id\n `,\n [parsed.linkId, scope.organizationId, scope.tenantId],\n )\n if (!reserved[0]?.id) {\n throw new CrudHttpError(422, { error: 'This payment link is no longer available' })\n }\n lockedLinkId = currentLink.id\n lockedLinkSlug = currentLink.slug\n lockedLinkTemplateId = currentLink.templateId ?? null\n lockedLinkGatewayProvider = currentLink.gatewayProviderKey ?? null\n shouldEmitLockedEvent = !currentLink.isLocked\n const transaction = tx.create(CheckoutTransaction, {\n ...parsed,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n amount: toMoneyString(parsed.amount) ?? '0.00',\n status: 'processing',\n })\n tx.persist(transaction)\n await tx.flush()\n return transaction\n })\n if (shouldEmitLockedEvent && lockedLinkId && lockedLinkSlug) {\n await emitCheckoutEvent('checkout.link.locked', {\n id: lockedLinkId,\n slug: lockedLinkSlug,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n }).catch(() => undefined)\n }\n await emitCheckoutEvent('checkout.transaction.created', {\n transactionId: transaction.id,\n linkId: transaction.linkId,\n status: transaction.status,\n amount: Number(transaction.amount),\n currency: transaction.currencyCode,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n }).catch(() => undefined)\n await emitCheckoutEvent('checkout.transaction.customerDataCaptured', {\n transactionId: transaction.id,\n linkId: transaction.linkId,\n templateId: lockedLinkTemplateId,\n slug: lockedLinkSlug,\n status: transaction.status,\n paymentStatus: transaction.paymentStatus ?? null,\n amount: Number(transaction.amount),\n currency: transaction.currencyCode,\n gatewayProvider: lockedLinkGatewayProvider,\n gatewayTransactionId: transaction.gatewayTransactionId ?? null,\n occurredAt: new Date().toISOString(),\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n customerDataCaptured: true,\n }).catch(() => undefined)\n return { id: transaction.id }\n },\n}\n\nconst updateTransactionStatusCommand: CommandHandler<Record<string, unknown>, { ok: true }> = {\n id: 'checkout.transaction.updateStatus',\n async execute(rawInput, ctx) {\n const { parsed } = parseCheckoutInput(rawInput, transactionUpdateStatusSchema.parse)\n const scope = resolveTransactionScope(parsed)\n const em = ctx.container.resolve('em') as EntityManager\n let terminalEventPayload: CheckoutTerminalEventPayload | null = null\n let emitUsageLimitReached = false\n let usageLimitReachedLinkId: string | null = null\n let usageLimitReachedLinkSlug: string | null = null\n await em.transactional(async (tx) => {\n const transaction = await tx.findOne(CheckoutTransaction, {\n id: parsed.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n if (!transaction) throw new CrudHttpError(404, { error: 'Transaction not found' })\n const link = await tx.findOne(CheckoutLink, {\n id: transaction.linkId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n })\n if (!link) throw new CrudHttpError(404, { error: 'Payment link not found' })\n\n const previousStatus = transaction.status\n const nextStatus = parsed.status\n const previousTerminal = isTerminalCheckoutStatus(previousStatus)\n const nextTerminal = isTerminalCheckoutStatus(nextStatus)\n\n // State-machine guard: reject any transition not permitted by the\n // VALID_CHECKOUT_TRANSITIONS map (e.g. completed \u2192 processing).\n // This runs inside the DB transaction so the check and the write share\n // the same serialisable snapshot.\n assertValidCheckoutStatusTransition(previousStatus, nextStatus)\n\n // Resolve the new field values before the write.\n const newPaymentStatus = parsed.paymentStatus ?? transaction.paymentStatus ?? null\n const newGatewayTransactionId = parsed.gatewayTransactionId ?? transaction.gatewayTransactionId ?? null\n\n // Atomic compare-and-swap: the WHERE clause pins the current status so\n // a concurrent writer that already advanced the status to a terminal\n // state will cause this update to match 0 rows \u2014 eliminating the TOCTOU\n // window between the findOne above and this write.\n const affected = await tx.nativeUpdate(\n CheckoutTransaction,\n {\n id: parsed.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n status: previousStatus,\n },\n {\n status: nextStatus,\n paymentStatus: newPaymentStatus,\n gatewayTransactionId: newGatewayTransactionId,\n updatedAt: new Date(),\n },\n )\n\n if (affected === 0) {\n // A concurrent writer already advanced the status. Force a DB round-trip\n // (refresh: true bypasses the MikroORM identity map) so the reported\n // currentStatus reflects the winning writer's value, not the stale snapshot.\n const actualTx = await tx.findOne(\n CheckoutTransaction,\n { id: parsed.id, organizationId: scope.organizationId, tenantId: scope.tenantId },\n { fields: ['status'], refresh: true },\n )\n throw new CrudHttpError(409, {\n error: `[internal] Transaction status was already updated by a concurrent process (expected \"${previousStatus}\", actual \"${actualTx?.status ?? 'unknown'}\")`,\n code: 'concurrent_status_update',\n expectedStatus: previousStatus,\n currentStatus: actualTx?.status ?? 'unknown',\n requestedStatus: nextStatus,\n })\n }\n\n // Sync the entity state in the unit of work by refreshing it.\n // This loads the updated values (status, paymentStatus, gatewayTransactionId)\n // from the DB and marks the entity as clean, avoiding a redundant second write\n // during the subsequent tx.flush().\n await tx.refresh(transaction)\n\n // Only apply terminal link state and emit the terminal event when the\n // status actually changes \u2014 prevents double-notification on idempotent\n // redeliveries (e.g. authorized \u2192 captured, both mapping to 'completed').\n if (!previousTerminal && nextTerminal) {\n const { usageLimitReached } = applyTerminalTransactionState(link, nextStatus)\n await tx.flush()\n if (usageLimitReached) {\n emitUsageLimitReached = true\n usageLimitReachedLinkId = link.id\n usageLimitReachedLinkSlug = link.slug\n }\n }\n if (nextTerminal && previousStatus !== nextStatus) {\n terminalEventPayload = {\n transactionId: transaction.id,\n linkId: transaction.linkId,\n templateId: link.templateId ?? null,\n slug: link.slug,\n status: transaction.status,\n paymentStatus: transaction.paymentStatus ?? null,\n amount: Number(transaction.amount),\n currency: transaction.currencyCode,\n gatewayProvider: link.gatewayProviderKey ?? null,\n gatewayTransactionId: transaction.gatewayTransactionId ?? null,\n occurredAt: new Date().toISOString(),\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n }\n }\n })\n if (terminalEventPayload !== null) {\n if (parsed.status === 'completed') {\n await emitCheckoutEvent('checkout.transaction.completed', terminalEventPayload).catch(() => undefined)\n } else if (parsed.status === 'failed') {\n await emitCheckoutEvent('checkout.transaction.failed', terminalEventPayload).catch(() => undefined)\n } else if (parsed.status === 'cancelled') {\n await emitCheckoutEvent('checkout.transaction.cancelled', terminalEventPayload).catch(() => undefined)\n } else if (parsed.status === 'expired') {\n await emitCheckoutEvent('checkout.transaction.expired', terminalEventPayload).catch(() => undefined)\n }\n }\n if (emitUsageLimitReached && usageLimitReachedLinkId && usageLimitReachedLinkSlug) {\n await emitCheckoutEvent('checkout.link.usageLimitReached', {\n id: usageLimitReachedLinkId,\n slug: usageLimitReachedLinkSlug,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n }).catch(() => undefined)\n }\n return { ok: true }\n },\n}\n\nregisterCommand(createTransactionCommand)\nregisterCommand(updateTransactionStatusCommand)\n\nexport function mapGatewayStatusForCommand(status: string | null | undefined) {\n return mapGatewayStatusToCheckoutStatus(status)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["transaction"]
7
7
  }
@@ -0,0 +1,31 @@
1
+ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
2
+ const VALID_CHECKOUT_TRANSITIONS = {
3
+ pending: ["processing", "completed", "failed", "cancelled", "expired"],
4
+ processing: ["completed", "failed", "cancelled", "expired"],
5
+ // Terminal states — no transitions out
6
+ completed: [],
7
+ failed: [],
8
+ cancelled: [],
9
+ expired: []
10
+ };
11
+ function isValidCheckoutStatusTransition(from, to) {
12
+ if (from === to) return true;
13
+ const allowed = VALID_CHECKOUT_TRANSITIONS[from];
14
+ if (!allowed) return false;
15
+ return allowed.includes(to);
16
+ }
17
+ function assertValidCheckoutStatusTransition(from, to) {
18
+ if (isValidCheckoutStatusTransition(from, to)) return;
19
+ throw new CrudHttpError(409, {
20
+ error: `[internal] Cannot transition payment from "${from}" to "${to}"`,
21
+ code: "invalid_status_transition",
22
+ currentStatus: from,
23
+ requestedStatus: to
24
+ });
25
+ }
26
+ export {
27
+ VALID_CHECKOUT_TRANSITIONS,
28
+ assertValidCheckoutStatusTransition,
29
+ isValidCheckoutStatusTransition
30
+ };
31
+ //# sourceMappingURL=transaction-status-machine.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/checkout/lib/transaction-status-machine.ts"],
4
+ "sourcesContent": ["import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CheckoutTransaction } from '../data/entities'\n\n/**\n * Allowed status transitions for a CheckoutTransaction.\n *\n * Terminal states (completed, failed, cancelled, expired) have an empty\n * allowed-set, meaning NO transition out of them is permitted. This prevents\n * the race condition where the submit route's late `updateStatus('processing')`\n * call overwrites a `'completed'` status that the gateway webhook set first.\n *\n * Allowed path:\n * pending \u2500\u2500\u25BA processing (transaction created, payment session initiated)\n * pending \u2500\u2500\u25BA completed (captured webhook arrives before processing state)\n * processing \u2500\u2500\u25BA completed (webhook: payment captured)\n * processing \u2500\u2500\u25BA failed (webhook or session error)\n * processing \u2500\u2500\u25BA cancelled (customer or merchant cancellation)\n * processing \u2500\u2500\u25BA expired (expiry worker)\n *\n * Rejected (regression guard):\n * completed \u2500\u2500\u25BA * REJECTED\n * failed \u2500\u2500\u25BA * REJECTED\n * cancelled \u2500\u2500\u25BA * REJECTED\n * expired \u2500\u2500\u25BA * REJECTED\n */\nexport const VALID_CHECKOUT_TRANSITIONS: Record<CheckoutTransaction['status'], CheckoutTransaction['status'][]> = {\n pending: ['processing', 'completed', 'failed', 'cancelled', 'expired'],\n processing: ['completed', 'failed', 'cancelled', 'expired'],\n // Terminal states \u2014 no transitions out\n completed: [],\n failed: [],\n cancelled: [],\n expired: [],\n}\n\n/**\n * Returns `true` when transitioning from `from` to `to` is allowed by the\n * state machine. Same-state transitions are allowed as safe no-ops to support\n * idempotency.\n */\nexport function isValidCheckoutStatusTransition(\n from: CheckoutTransaction['status'],\n to: CheckoutTransaction['status'],\n): boolean {\n if (from === to) return true\n const allowed = VALID_CHECKOUT_TRANSITIONS[from]\n if (!allowed) return false\n return allowed.includes(to)\n}\n\n/**\n * Throws a structured 409 Conflict when the requested status transition is\n * not allowed by the state machine. The error body is compatible with the\n * `conflict` error shape that the frontend handles.\n *\n * This must be called **inside** a database transaction so that the check and\n * the subsequent write are atomic. The atomic `nativeUpdate` WHERE-clause\n * provides the TOCTOU safety net; this function is the human-readable guard.\n */\nexport function assertValidCheckoutStatusTransition(\n from: CheckoutTransaction['status'],\n to: CheckoutTransaction['status'],\n): void {\n if (isValidCheckoutStatusTransition(from, to)) return\n throw new CrudHttpError(409, {\n error: `[internal] Cannot transition payment from \"${from}\" to \"${to}\"`,\n code: 'invalid_status_transition',\n currentStatus: from,\n requestedStatus: to,\n })\n}\n"],
5
+ "mappings": "AAAA,SAAS,qBAAqB;AAyBvB,MAAM,6BAAqG;AAAA,EAChH,SAAS,CAAC,cAAc,aAAa,UAAU,aAAa,SAAS;AAAA,EACrE,YAAY,CAAC,aAAa,UAAU,aAAa,SAAS;AAAA;AAAA,EAE1D,WAAW,CAAC;AAAA,EACZ,QAAQ,CAAC;AAAA,EACT,WAAW,CAAC;AAAA,EACZ,SAAS,CAAC;AACZ;AAOO,SAAS,gCACd,MACA,IACS;AACT,MAAI,SAAS,GAAI,QAAO;AACxB,QAAM,UAAU,2BAA2B,IAAI;AAC/C,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,SAAS,EAAE;AAC5B;AAWO,SAAS,oCACd,MACA,IACM;AACN,MAAI,gCAAgC,MAAM,EAAE,EAAG;AAC/C,QAAM,IAAI,cAAc,KAAK;AAAA,IAC3B,OAAO,8CAA8C,IAAI,SAAS,EAAE;AAAA,IACpE,MAAM;AAAA,IACN,eAAe;AAAA,IACf,iBAAiB;AAAA,EACnB,CAAC;AACH;",
6
+ "names": []
7
+ }