@open-mercato/checkout 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7

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.
Files changed (63) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/checkout/api/pay/[slug]/submit/route.js +28 -43
  3. package/dist/modules/checkout/api/pay/[slug]/submit/route.js.map +2 -2
  4. package/dist/modules/checkout/api/transactions/route.js +6 -23
  5. package/dist/modules/checkout/api/transactions/route.js.map +2 -2
  6. package/dist/modules/checkout/backend/checkout/pay-links/page.js +1 -2
  7. package/dist/modules/checkout/backend/checkout/pay-links/page.js.map +2 -2
  8. package/dist/modules/checkout/backend/checkout/templates/page.js +1 -2
  9. package/dist/modules/checkout/backend/checkout/templates/page.js.map +2 -2
  10. package/dist/modules/checkout/backend/checkout/transactions/page.js +1 -2
  11. package/dist/modules/checkout/backend/checkout/transactions/page.js.map +2 -2
  12. package/dist/modules/checkout/commands/links.js +1 -1
  13. package/dist/modules/checkout/commands/links.js.map +2 -2
  14. package/dist/modules/checkout/commands/transactions.js +13 -45
  15. package/dist/modules/checkout/commands/transactions.js.map +2 -2
  16. package/dist/modules/checkout/components/PayPage.js +38 -39
  17. package/dist/modules/checkout/components/PayPage.js.map +2 -2
  18. package/dist/modules/checkout/search.js +6 -44
  19. package/dist/modules/checkout/search.js.map +2 -2
  20. package/dist/modules/checkout/setup.js +0 -28
  21. package/dist/modules/checkout/setup.js.map +2 -2
  22. package/dist/modules/checkout/workers/transaction-expiry.worker.js +2 -9
  23. package/dist/modules/checkout/workers/transaction-expiry.worker.js.map +2 -2
  24. package/package.json +9 -10
  25. package/src/modules/checkout/api/pay/[slug]/submit/__tests__/route.test.ts +0 -77
  26. package/src/modules/checkout/api/pay/[slug]/submit/route.ts +28 -58
  27. package/src/modules/checkout/api/transactions/route.ts +6 -30
  28. package/src/modules/checkout/backend/checkout/pay-links/page.tsx +1 -2
  29. package/src/modules/checkout/backend/checkout/templates/page.tsx +1 -2
  30. package/src/modules/checkout/backend/checkout/transactions/page.tsx +1 -2
  31. package/src/modules/checkout/commands/links.ts +1 -5
  32. package/src/modules/checkout/commands/transactions.ts +13 -68
  33. package/src/modules/checkout/components/PayPage.tsx +37 -38
  34. package/src/modules/checkout/i18n/de.json +0 -1
  35. package/src/modules/checkout/i18n/en.json +0 -1
  36. package/src/modules/checkout/i18n/es.json +0 -1
  37. package/src/modules/checkout/i18n/pl.json +0 -1
  38. package/src/modules/checkout/search.ts +6 -44
  39. package/src/modules/checkout/setup.ts +0 -51
  40. package/src/modules/checkout/workers/transaction-expiry.worker.ts +0 -11
  41. package/dist/modules/checkout/__integration__/TC-CHKT-042.spec.js +0 -39
  42. package/dist/modules/checkout/__integration__/TC-CHKT-042.spec.js.map +0 -7
  43. package/dist/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.js +0 -62
  44. package/dist/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.js.map +0 -7
  45. package/dist/modules/checkout/extension-points.js +0 -68
  46. package/dist/modules/checkout/extension-points.js.map +0 -7
  47. package/dist/modules/checkout/lib/transaction-status-machine.js +0 -31
  48. package/dist/modules/checkout/lib/transaction-status-machine.js.map +0 -7
  49. package/dist/modules/checkout/subscribers/search-reindex-transaction-created.js +0 -32
  50. package/dist/modules/checkout/subscribers/search-reindex-transaction-created.js.map +0 -7
  51. package/src/modules/checkout/__integration__/TC-CHKT-042.spec.ts +0 -41
  52. package/src/modules/checkout/__integration__/TC-CHKT-043-concurrent-status-update.spec.ts +0 -112
  53. package/src/modules/checkout/__tests__/search.test.ts +0 -21
  54. package/src/modules/checkout/__tests__/setup-schedules.test.ts +0 -99
  55. package/src/modules/checkout/api/transactions/__tests__/route.test.ts +0 -113
  56. package/src/modules/checkout/commands/__tests__/update-transaction-status.test.ts +0 -363
  57. package/src/modules/checkout/extension-points.ts +0 -67
  58. package/src/modules/checkout/i18n/ko.json +0 -463
  59. package/src/modules/checkout/lib/__tests__/transaction-status-machine.test.ts +0 -152
  60. package/src/modules/checkout/lib/transaction-status-machine.ts +0 -71
  61. package/src/modules/checkout/subscribers/__tests__/search-reindex-transaction-created.test.ts +0 -29
  62. package/src/modules/checkout/subscribers/search-reindex-transaction-created.ts +0 -39
  63. package/src/modules/checkout/workers/__tests__/transaction-expiry.worker.test.ts +0 -124
@@ -12,7 +12,6 @@ import {
12
12
  parseCheckoutInput,
13
13
  toMoneyString,
14
14
  } from '../lib/utils'
15
- import { assertValidCheckoutStatusTransition } from '../lib/transaction-status-machine'
16
15
 
17
16
  function resolveTransactionScope(input: { tenantId?: string | null; organizationId?: string | null }) {
18
17
  if (!input.organizationId || !input.tenantId) {
@@ -169,63 +168,11 @@ const updateTransactionStatusCommand: CommandHandler<Record<string, unknown>, {
169
168
  const previousTerminal = isTerminalCheckoutStatus(previousStatus)
170
169
  const nextTerminal = isTerminalCheckoutStatus(nextStatus)
171
170
 
172
- // State-machine guard: reject any transition not permitted by the
173
- // VALID_CHECKOUT_TRANSITIONS map (e.g. completed → processing).
174
- // This runs inside the DB transaction so the check and the write share
175
- // the same serialisable snapshot.
176
- assertValidCheckoutStatusTransition(previousStatus, nextStatus)
177
-
178
- // Resolve the new field values before the write.
179
- const newPaymentStatus = parsed.paymentStatus ?? transaction.paymentStatus ?? null
180
- const newGatewayTransactionId = parsed.gatewayTransactionId ?? transaction.gatewayTransactionId ?? null
181
-
182
- // Atomic compare-and-swap: the WHERE clause pins the current status so
183
- // a concurrent writer that already advanced the status to a terminal
184
- // state will cause this update to match 0 rows — eliminating the TOCTOU
185
- // window between the findOne above and this write.
186
- const affected = await tx.nativeUpdate(
187
- CheckoutTransaction,
188
- {
189
- id: parsed.id,
190
- organizationId: scope.organizationId,
191
- tenantId: scope.tenantId,
192
- status: previousStatus,
193
- },
194
- {
195
- status: nextStatus,
196
- paymentStatus: newPaymentStatus,
197
- gatewayTransactionId: newGatewayTransactionId,
198
- updatedAt: new Date(),
199
- },
200
- )
201
-
202
- if (affected === 0) {
203
- // A concurrent writer already advanced the status. Force a DB round-trip
204
- // (refresh: true bypasses the MikroORM identity map) so the reported
205
- // currentStatus reflects the winning writer's value, not the stale snapshot.
206
- const actualTx = await tx.findOne(
207
- CheckoutTransaction,
208
- { id: parsed.id, organizationId: scope.organizationId, tenantId: scope.tenantId },
209
- { fields: ['status'], refresh: true },
210
- )
211
- throw new CrudHttpError(409, {
212
- error: `[internal] Transaction status was already updated by a concurrent process (expected "${previousStatus}", actual "${actualTx?.status ?? 'unknown'}")`,
213
- code: 'concurrent_status_update',
214
- expectedStatus: previousStatus,
215
- currentStatus: actualTx?.status ?? 'unknown',
216
- requestedStatus: nextStatus,
217
- })
218
- }
219
-
220
- // Sync the entity state in the unit of work by refreshing it.
221
- // This loads the updated values (status, paymentStatus, gatewayTransactionId)
222
- // from the DB and marks the entity as clean, avoiding a redundant second write
223
- // during the subsequent tx.flush().
224
- await tx.refresh(transaction)
171
+ transaction.status = nextStatus
172
+ transaction.paymentStatus = parsed.paymentStatus ?? transaction.paymentStatus ?? null
173
+ transaction.gatewayTransactionId = parsed.gatewayTransactionId ?? transaction.gatewayTransactionId ?? null
174
+ await tx.flush()
225
175
 
226
- // Only apply terminal link state and emit the terminal event when the
227
- // status actually changes — prevents double-notification on idempotent
228
- // redeliveries (e.g. authorized → captured, both mapping to 'completed').
229
176
  if (!previousTerminal && nextTerminal) {
230
177
  const { usageLimitReached } = applyTerminalTransactionState(link, nextStatus)
231
178
  await tx.flush()
@@ -235,7 +182,7 @@ const updateTransactionStatusCommand: CommandHandler<Record<string, unknown>, {
235
182
  usageLimitReachedLinkSlug = link.slug
236
183
  }
237
184
  }
238
- if (nextTerminal && previousStatus !== nextStatus) {
185
+ if (nextTerminal) {
239
186
  terminalEventPayload = {
240
187
  transactionId: transaction.id,
241
188
  linkId: transaction.linkId,
@@ -253,16 +200,14 @@ const updateTransactionStatusCommand: CommandHandler<Record<string, unknown>, {
253
200
  }
254
201
  }
255
202
  })
256
- if (terminalEventPayload !== null) {
257
- if (parsed.status === 'completed') {
258
- await emitCheckoutEvent('checkout.transaction.completed', terminalEventPayload).catch(() => undefined)
259
- } else if (parsed.status === 'failed') {
260
- await emitCheckoutEvent('checkout.transaction.failed', terminalEventPayload).catch(() => undefined)
261
- } else if (parsed.status === 'cancelled') {
262
- await emitCheckoutEvent('checkout.transaction.cancelled', terminalEventPayload).catch(() => undefined)
263
- } else if (parsed.status === 'expired') {
264
- await emitCheckoutEvent('checkout.transaction.expired', terminalEventPayload).catch(() => undefined)
265
- }
203
+ if (parsed.status === 'completed') {
204
+ await emitCheckoutEvent('checkout.transaction.completed', terminalEventPayload ?? {}).catch(() => undefined)
205
+ } else if (parsed.status === 'failed') {
206
+ await emitCheckoutEvent('checkout.transaction.failed', terminalEventPayload ?? {}).catch(() => undefined)
207
+ } else if (parsed.status === 'cancelled') {
208
+ await emitCheckoutEvent('checkout.transaction.cancelled', terminalEventPayload ?? {}).catch(() => undefined)
209
+ } else if (parsed.status === 'expired') {
210
+ await emitCheckoutEvent('checkout.transaction.expired', terminalEventPayload ?? {}).catch(() => undefined)
266
211
  }
267
212
  if (emitUsageLimitReached && usageLimitReachedLinkId && usageLimitReachedLinkSlug) {
268
213
  await emitCheckoutEvent('checkout.link.usageLimitReached', {
@@ -1,9 +1,9 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { extensionPoints } from '@open-mercato/checkout/modules/checkout/extension-points'
5
4
  import Link from 'next/link'
6
5
  import { useParams, useRouter, useSearchParams } from 'next/navigation'
6
+ import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
7
7
  import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
8
8
  import { locales, type Locale } from '@open-mercato/shared/lib/i18n/config'
9
9
  import type { CustomFieldDisplayEntry } from '@open-mercato/shared/lib/crud/custom-fields'
@@ -270,17 +270,17 @@ export type PayPageFooterProps = {
270
270
  themeTokens: PayPageThemeTokens
271
271
  }
272
272
 
273
- const PAGE_HANDLE = extensionPoints.hosts.payPage.componentId
274
- const HEADER_HANDLE = extensionPoints.hosts.header.componentId
275
- const DESCRIPTION_HANDLE = extensionPoints.hosts.description.componentId
276
- const SUMMARY_HANDLE = extensionPoints.hosts.summary.componentId
277
- const PRICING_HANDLE = extensionPoints.hosts.pricing.componentId
278
- const PAYMENT_HANDLE = extensionPoints.hosts.payment.componentId
279
- const CUSTOMER_FORM_HANDLE = extensionPoints.hosts.customerForm.componentId
280
- const LEGAL_CONSENT_HANDLE = extensionPoints.hosts.legalConsent.componentId
281
- const GATEWAY_FORM_HANDLE = extensionPoints.hosts.gatewayForm.componentId
282
- const HELP_HANDLE = extensionPoints.hosts.help.componentId
283
- const FOOTER_HANDLE = extensionPoints.hosts.footer.componentId
273
+ const PAGE_HANDLE = ComponentReplacementHandles.page('checkout.pay-page')
274
+ const HEADER_HANDLE = ComponentReplacementHandles.section('checkout.pay-page', 'header')
275
+ const DESCRIPTION_HANDLE = ComponentReplacementHandles.section('checkout.pay-page', 'description')
276
+ const SUMMARY_HANDLE = ComponentReplacementHandles.section('checkout.pay-page', 'summary')
277
+ const PRICING_HANDLE = ComponentReplacementHandles.section('checkout.pay-page', 'pricing')
278
+ const PAYMENT_HANDLE = ComponentReplacementHandles.section('checkout.pay-page', 'payment')
279
+ const CUSTOMER_FORM_HANDLE = ComponentReplacementHandles.section('checkout.pay-page', 'customer-form')
280
+ const LEGAL_CONSENT_HANDLE = ComponentReplacementHandles.section('checkout.pay-page', 'legal-consent')
281
+ const GATEWAY_FORM_HANDLE = ComponentReplacementHandles.section('checkout.pay-page', 'gateway-form')
282
+ const HELP_HANDLE = ComponentReplacementHandles.section('checkout.pay-page', 'help')
283
+ const FOOTER_HANDLE = ComponentReplacementHandles.section('checkout.pay-page', 'footer')
284
284
 
285
285
  // Checkout supports merchant branding defaults; keep those theme fallbacks isolated from general UI state colors.
286
286
  const CHECKOUT_THEME_FALLBACKS = {
@@ -1222,10 +1222,10 @@ export function PayPagePaymentForm({
1222
1222
  </Alert>
1223
1223
  ) : null}
1224
1224
 
1225
- <InjectionSpot spotId={extensionPoints.hosts.gatewayWidgetBefore.spotId} context={injectionContext} />
1225
+ <InjectionSpot spotId="checkout.pay-page:gateway-widget:before" context={injectionContext} />
1226
1226
  {embeddedPaymentSession && embeddedRenderer ? (
1227
1227
  <>
1228
- <InjectionSpot spotId={extensionPoints.hosts.gatewayRendererBefore.spotId} context={injectionContext} />
1228
+ <InjectionSpot spotId="checkout.pay-page:gateway-widget:renderer:before" context={injectionContext} />
1229
1229
  {React.createElement(embeddedRenderer, {
1230
1230
  providerKey: embeddedPaymentSession.providerKey ?? '',
1231
1231
  transactionId: activeTransactionId ?? '',
@@ -1235,11 +1235,11 @@ export function PayPagePaymentForm({
1235
1235
  onComplete,
1236
1236
  onError,
1237
1237
  })}
1238
- <InjectionSpot spotId={extensionPoints.hosts.gatewayRendererAfter.spotId} context={injectionContext} />
1238
+ <InjectionSpot spotId="checkout.pay-page:gateway-widget:renderer:after" context={injectionContext} />
1239
1239
  </>
1240
1240
  ) : (
1241
1241
  <>
1242
- <InjectionSpot spotId={extensionPoints.hosts.gatewayActionsBefore.spotId} context={injectionContext} />
1242
+ <InjectionSpot spotId="checkout.pay-page:gateway-widget:actions:before" context={injectionContext} />
1243
1243
  <Button
1244
1244
  type="button"
1245
1245
  className="h-12 w-full rounded-xl text-base"
@@ -1256,10 +1256,10 @@ export function PayPagePaymentForm({
1256
1256
  : t('checkout.payPage.actions.payNow', 'Pay now')}
1257
1257
  </span>
1258
1258
  </Button>
1259
- <InjectionSpot spotId={extensionPoints.hosts.gatewayActionsAfter.spotId} context={injectionContext} />
1259
+ <InjectionSpot spotId="checkout.pay-page:gateway-widget:actions:after" context={injectionContext} />
1260
1260
  </>
1261
1261
  )}
1262
- <InjectionSpot spotId={extensionPoints.hosts.gatewayWidgetAfter.spotId} context={injectionContext} />
1262
+ <InjectionSpot spotId="checkout.pay-page:gateway-widget:after" context={injectionContext} />
1263
1263
 
1264
1264
  {paymentSession ? (
1265
1265
  <Button
@@ -1369,7 +1369,6 @@ export function PayPageFooter({ payload, themeTokens }: PayPageFooterProps) {
1369
1369
  pl: t('common.languages.polish', 'Polski'),
1370
1370
  es: t('common.languages.spanish', 'Español'),
1371
1371
  de: t('common.languages.german', 'Deutsch'),
1372
- ko: t('common.languages.korean', '한국어'),
1373
1372
  }), [t])
1374
1373
 
1375
1374
  const setLocale = React.useCallback(async (nextLocale: Locale) => {
@@ -2145,11 +2144,11 @@ export function PayPage({
2145
2144
 
2146
2145
  const leftColumn = (
2147
2146
  <div className="space-y-6">
2148
- <InjectionSpot spotId={extensionPoints.hosts.headerBefore.spotId} context={injectionContext} />
2147
+ <InjectionSpot spotId="checkout.pay-page:header:before" context={injectionContext} />
2149
2148
  <div data-component-handle={HEADER_HANDLE}>
2150
2149
  <HeaderComponent payload={payload} preview={isPreview} themeTokens={themeTokens} />
2151
2150
  </div>
2152
- <InjectionSpot spotId={extensionPoints.hosts.headerAfter.spotId} context={injectionContext} />
2151
+ <InjectionSpot spotId="checkout.pay-page:header:after" context={injectionContext} />
2153
2152
 
2154
2153
  {(payload.description || publicCustomFields.length > 0) ? (
2155
2154
  <>
@@ -2160,11 +2159,11 @@ export function PayPage({
2160
2159
  themeTokens={themeTokens}
2161
2160
  />
2162
2161
  </div>
2163
- <InjectionSpot spotId={extensionPoints.hosts.descriptionAfter.spotId} context={injectionContext} />
2162
+ <InjectionSpot spotId="checkout.pay-page:description:after" context={injectionContext} />
2164
2163
  </>
2165
2164
  ) : null}
2166
2165
 
2167
- <InjectionSpot spotId={extensionPoints.hosts.customerFieldsBefore.spotId} context={injectionContext} />
2166
+ <InjectionSpot spotId="checkout.pay-page:customer-fields:before" context={injectionContext} />
2168
2167
  {shouldCollectCustomerDetails ? (
2169
2168
  <div data-component-handle={CUSTOMER_FORM_HANDLE}>
2170
2169
  <CustomerFormComponent
@@ -2178,13 +2177,13 @@ export function PayPage({
2178
2177
  />
2179
2178
  </div>
2180
2179
  ) : null}
2181
- <InjectionSpot spotId={extensionPoints.hosts.customerFieldsAfter.spotId} context={injectionContext} />
2180
+ <InjectionSpot spotId="checkout.pay-page:customer-fields:after" context={injectionContext} />
2182
2181
  </div>
2183
2182
  )
2184
2183
 
2185
2184
  const paymentFlow = (
2186
2185
  <>
2187
- <InjectionSpot spotId={extensionPoints.hosts.pricingBefore.spotId} context={injectionContext} />
2186
+ <InjectionSpot spotId="checkout.pay-page:pricing:before" context={injectionContext} />
2188
2187
  <div data-component-handle={PRICING_HANDLE}>
2189
2188
  <PricingComponent
2190
2189
  payload={payload}
@@ -2216,9 +2215,9 @@ export function PayPage({
2216
2215
  themeTokens={themeTokens}
2217
2216
  />
2218
2217
  </div>
2219
- <InjectionSpot spotId={extensionPoints.hosts.pricingAfter.spotId} context={injectionContext} />
2218
+ <InjectionSpot spotId="checkout.pay-page:pricing:after" context={injectionContext} />
2220
2219
 
2221
- <InjectionSpot spotId={extensionPoints.hosts.summaryBefore.spotId} context={injectionContext} />
2220
+ <InjectionSpot spotId="checkout.pay-page:summary:before" context={injectionContext} />
2222
2221
  <div data-component-handle={SUMMARY_HANDLE}>
2223
2222
  <SummaryComponent
2224
2223
  payload={payload}
@@ -2229,9 +2228,9 @@ export function PayPage({
2229
2228
  themeTokens={themeTokens}
2230
2229
  />
2231
2230
  </div>
2232
- <InjectionSpot spotId={extensionPoints.hosts.summaryAfter.spotId} context={injectionContext} />
2231
+ <InjectionSpot spotId="checkout.pay-page:summary:after" context={injectionContext} />
2233
2232
 
2234
- <InjectionSpot spotId={extensionPoints.hosts.legalConsentBefore.spotId} context={injectionContext} />
2233
+ <InjectionSpot spotId="checkout.pay-page:legal-consent:before" context={injectionContext} />
2235
2234
  <div data-component-handle={LEGAL_CONSENT_HANDLE}>
2236
2235
  <LegalConsentComponent
2237
2236
  payload={payload}
@@ -2243,9 +2242,9 @@ export function PayPage({
2243
2242
  themeTokens={themeTokens}
2244
2243
  />
2245
2244
  </div>
2246
- <InjectionSpot spotId={extensionPoints.hosts.legalConsentAfter.spotId} context={injectionContext} />
2245
+ <InjectionSpot spotId="checkout.pay-page:legal-consent:after" context={injectionContext} />
2247
2246
 
2248
- <InjectionSpot spotId={extensionPoints.hosts.submitBefore.spotId} context={injectionContext} />
2247
+ <InjectionSpot spotId="checkout.pay-page:submit:before" context={injectionContext} />
2249
2248
  <div data-component-handle={GATEWAY_FORM_HANDLE}>
2250
2249
  <PaymentFormComponent
2251
2250
  payload={payload}
@@ -2273,35 +2272,35 @@ export function PayPage({
2273
2272
  themeTokens={themeTokens}
2274
2273
  />
2275
2274
  </div>
2276
- <InjectionSpot spotId={extensionPoints.hosts.submitAfter.spotId} context={injectionContext} />
2275
+ <InjectionSpot spotId="checkout.pay-page:submit:after" context={injectionContext} />
2277
2276
 
2278
- <InjectionSpot spotId={extensionPoints.hosts.helpBefore.spotId} context={injectionContext} />
2277
+ <InjectionSpot spotId="checkout.pay-page:help:before" context={injectionContext} />
2279
2278
  <div data-component-handle={HELP_HANDLE}>
2280
2279
  <HelpComponent payload={payload} preview={isPreview} themeTokens={themeTokens} />
2281
2280
  </div>
2282
- <InjectionSpot spotId={extensionPoints.hosts.helpAfter.spotId} context={injectionContext} />
2281
+ <InjectionSpot spotId="checkout.pay-page:help:after" context={injectionContext} />
2283
2282
  </>
2284
2283
  )
2285
2284
 
2286
2285
  const rightColumn = (
2287
2286
  <>
2288
- <InjectionSpot spotId={extensionPoints.hosts.paymentBefore.spotId} context={injectionContext} />
2287
+ <InjectionSpot spotId="checkout.pay-page:payment:before" context={injectionContext} />
2289
2288
  <div data-component-handle={PAYMENT_HANDLE}>
2290
2289
  <PaymentSectionComponent payload={payload} preview={isPreview} themeTokens={themeTokens}>
2291
2290
  {paymentFlow}
2292
2291
  </PaymentSectionComponent>
2293
2292
  </div>
2294
- <InjectionSpot spotId={extensionPoints.hosts.paymentAfter.spotId} context={injectionContext} />
2293
+ <InjectionSpot spotId="checkout.pay-page:payment:after" context={injectionContext} />
2295
2294
  </>
2296
2295
  )
2297
2296
 
2298
2297
  const footer = (
2299
2298
  <>
2300
- <InjectionSpot spotId={extensionPoints.hosts.footerBefore.spotId} context={injectionContext} />
2299
+ <InjectionSpot spotId="checkout.pay-page:footer:before" context={injectionContext} />
2301
2300
  <div data-component-handle={FOOTER_HANDLE}>
2302
2301
  <FooterComponent payload={payload} themeTokens={themeTokens} />
2303
2302
  </div>
2304
- <InjectionSpot spotId={extensionPoints.hosts.footerAfter.spotId} context={injectionContext} />
2303
+ <InjectionSpot spotId="checkout.pay-page:footer:after" context={injectionContext} />
2305
2304
  </>
2306
2305
  )
2307
2306
 
@@ -380,7 +380,6 @@
380
380
  "checkout.payPage.validation.priceSelectionRequired": "Bitte eine Zahlungsoption auswaehlen, um fortzufahren.",
381
381
  "checkout.payPage.validation.requiredField": "Bitte {field} ausfuellen.",
382
382
  "checkout.payPage.validation.thisField": "dieses Feld",
383
- "checkout.search.subtitle.linkTemplate": "Link-Vorlage",
384
383
  "checkout.statusPage.backToPayment": "Zuruck zur Zahlungsseite",
385
384
  "checkout.statusPage.cancel.cancelled": "Zahlung abgebrochen",
386
385
  "checkout.statusPage.cancel.failed": "Zahlung fehlgeschlagen",
@@ -380,7 +380,6 @@
380
380
  "checkout.payPage.validation.priceSelectionRequired": "Choose a payment option to continue.",
381
381
  "checkout.payPage.validation.requiredField": "Enter {field}.",
382
382
  "checkout.payPage.validation.thisField": "this field",
383
- "checkout.search.subtitle.linkTemplate": "Link Template",
384
383
  "checkout.statusPage.backToPayment": "Back to payment page",
385
384
  "checkout.statusPage.cancel.cancelled": "Payment cancelled",
386
385
  "checkout.statusPage.cancel.failed": "Payment failed",
@@ -380,7 +380,6 @@
380
380
  "checkout.payPage.validation.priceSelectionRequired": "Elige una opcion de pago para continuar.",
381
381
  "checkout.payPage.validation.requiredField": "Completa {field}.",
382
382
  "checkout.payPage.validation.thisField": "este campo",
383
- "checkout.search.subtitle.linkTemplate": "Plantilla de enlace",
384
383
  "checkout.statusPage.backToPayment": "Volver a la pagina de pago",
385
384
  "checkout.statusPage.cancel.cancelled": "Pago cancelado",
386
385
  "checkout.statusPage.cancel.failed": "Pago fallido",
@@ -380,7 +380,6 @@
380
380
  "checkout.payPage.validation.priceSelectionRequired": "Wybierz opcję płatności, aby kontynuować.",
381
381
  "checkout.payPage.validation.requiredField": "Uzupełnij pole {field}.",
382
382
  "checkout.payPage.validation.thisField": "to pole",
383
- "checkout.search.subtitle.linkTemplate": "Szablon linku",
384
383
  "checkout.statusPage.backToPayment": "Wróć do strony płatności",
385
384
  "checkout.statusPage.cancel.cancelled": "Płatność anulowana",
386
385
  "checkout.statusPage.cancel.failed": "Płatność nieudana",
@@ -1,5 +1,4 @@
1
1
  import type { SearchModuleConfig } from '@open-mercato/shared/modules/search'
2
- import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
3
2
  import { CHECKOUT_ENTITY_IDS } from './lib/constants'
4
3
 
5
4
  function asSearchText(value: unknown): string {
@@ -35,52 +34,15 @@ export const searchConfig: SearchModuleConfig = {
35
34
  searchable: ['name', 'title'],
36
35
  excluded: ['passwordHash', 'gatewaySettings', 'customerFieldsSchema'],
37
36
  },
38
- buildSource: async (ctx) => {
39
- const { t } = await resolveTranslations()
40
- const linkTemplate = t('checkout.search.subtitle.linkTemplate', 'Link Template')
41
- return {
42
- text: [`${asSearchText(ctx.record.name)}: ${asSearchText(ctx.record.title)}`],
43
- presenter: { title: asSearchText(ctx.record.name), subtitle: linkTemplate },
44
- checksumSource: { record: ctx.record, customFields: ctx.customFields },
45
- }
46
- },
47
- formatResult: async (ctx) => {
48
- const { t } = await resolveTranslations()
49
- return {
50
- title: asSearchText(ctx.record.name),
51
- subtitle: t('checkout.search.subtitle.linkTemplate', 'Link Template'),
52
- icon: 'lucide:file-text',
53
- }
54
- },
55
- },
56
- {
57
- entityId: CHECKOUT_ENTITY_IDS.transaction,
58
- enabled: true,
59
- priority: 6,
60
- fieldPolicy: {
61
- searchable: ['email', 'firstName', 'lastName'],
62
- },
63
37
  buildSource: async (ctx) => ({
64
- text: [
65
- asSearchText(ctx.record.email),
66
- asSearchText(ctx.record.firstName),
67
- asSearchText(ctx.record.lastName),
68
- ],
69
- fields: {
70
- email: ctx.record.email,
71
- first_name: ctx.record.firstName,
72
- last_name: ctx.record.lastName,
73
- },
74
- presenter: {
75
- title: asSearchText(ctx.record.email),
76
- subtitle: `${asSearchText(ctx.record.firstName)} ${asSearchText(ctx.record.lastName)}`.trim(),
77
- },
78
- checksumSource: { record: ctx.record },
38
+ text: [`${asSearchText(ctx.record.name)}: ${asSearchText(ctx.record.title)}`],
39
+ presenter: { title: asSearchText(ctx.record.name), subtitle: 'Link Template' },
40
+ checksumSource: { record: ctx.record, customFields: ctx.customFields },
79
41
  }),
80
42
  formatResult: async (ctx) => ({
81
- title: asSearchText(ctx.record.email),
82
- subtitle: `${asSearchText(ctx.record.firstName)} ${asSearchText(ctx.record.lastName)}`.trim(),
83
- icon: 'lucide:receipt',
43
+ title: asSearchText(ctx.record.name),
44
+ subtitle: 'Link Template',
45
+ icon: 'lucide:file-text',
84
46
  }),
85
47
  },
86
48
  ],
@@ -1,65 +1,14 @@
1
1
  import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
2
- import { createHash } from 'node:crypto'
3
2
  import { ensureCheckoutFieldsetsAndDefinitions } from './seed/customFields'
4
3
  import { seedCheckoutExamples } from './seed/examples'
5
- import { CHECKOUT_EXPIRY_QUEUE } from './workers/transaction-expiry.worker'
6
4
  export { DEFAULT_CHECKOUT_CUSTOMER_FIELDS } from './lib/defaults'
7
5
 
8
- function stableUuidFromString(input: string): string {
9
- const bytes = createHash('sha256').update(input).digest().subarray(0, 16)
10
- bytes[6] = (bytes[6] & 0x0f) | 0x50
11
- bytes[8] = (bytes[8] & 0x3f) | 0x80
12
- const hex = Buffer.from(bytes).toString('hex')
13
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`
14
- }
15
-
16
- type SchedulerServiceLike = {
17
- register: (registration: {
18
- id: string
19
- name: string
20
- description?: string
21
- scopeType: 'organization'
22
- organizationId: string
23
- tenantId: string
24
- scheduleType: 'cron' | 'interval'
25
- scheduleValue: string
26
- timezone?: string
27
- targetType: 'queue'
28
- targetQueue: string
29
- targetPayload?: Record<string, unknown>
30
- sourceType: 'module'
31
- sourceModule: string
32
- isEnabled?: boolean
33
- }) => Promise<void>
34
- }
35
-
36
6
  export const setup: ModuleSetupConfig = {
37
7
  async seedDefaults(ctx) {
38
8
  await ensureCheckoutFieldsetsAndDefinitions(ctx.em, {
39
9
  tenantId: ctx.tenantId,
40
10
  organizationId: ctx.organizationId,
41
11
  })
42
-
43
- const cradle = ctx.container as { hasRegistration?: (name: string) => boolean }
44
- if (typeof cradle.hasRegistration !== 'function' || !cradle.hasRegistration('schedulerService')) return
45
-
46
- const schedulerService = ctx.container.resolve('schedulerService') as SchedulerServiceLike
47
- await schedulerService.register({
48
- id: stableUuidFromString(`checkout:transaction-expiry:${ctx.tenantId}:${ctx.organizationId}`),
49
- name: 'Checkout transaction expiry',
50
- description: 'Marks stale checkout transactions as expired and runs usage limit updates.',
51
- scopeType: 'organization',
52
- organizationId: ctx.organizationId,
53
- tenantId: ctx.tenantId,
54
- scheduleType: 'interval',
55
- scheduleValue: '10m',
56
- timezone: 'UTC',
57
- targetType: 'queue',
58
- targetQueue: CHECKOUT_EXPIRY_QUEUE,
59
- sourceType: 'module',
60
- sourceModule: 'checkout',
61
- isEnabled: true,
62
- })
63
12
  },
64
13
 
65
14
  defaultRoleFeatures: {
@@ -14,8 +14,6 @@ const EXPIRY_TIMEOUT_MS = 2 * 60 * 60 * 1000 // 2 hours
14
14
 
15
15
  export type CheckoutExpiryJob = {
16
16
  batchSize?: number
17
- tenantId: string
18
- organizationId: string
19
17
  }
20
18
 
21
19
  export const metadata: WorkerMeta = {
@@ -34,23 +32,14 @@ export default async function handle(job: QueuedJob<CheckoutExpiryJob>, ctx: Han
34
32
  const batchSize = job.payload?.batchSize ?? 100
35
33
  const cutoff = new Date(Date.now() - EXPIRY_TIMEOUT_MS)
36
34
 
37
- if (!job.payload?.tenantId || !job.payload?.organizationId) {
38
- throw new Error('[internal] tenantId and organizationId are required in CheckoutExpiryJob')
39
- }
40
-
41
- const { tenantId, organizationId } = job.payload
42
-
43
35
  const staleTransactions = await findWithDecryption(
44
36
  em,
45
37
  CheckoutTransaction,
46
38
  {
47
39
  status: 'processing',
48
40
  createdAt: { $lt: cutoff },
49
- tenantId,
50
- organizationId,
51
41
  },
52
42
  { limit: batchSize, orderBy: { createdAt: 'ASC' } },
53
- { tenantId, organizationId },
54
43
  )
55
44
 
56
45
  for (const transaction of staleTransactions) {
@@ -1,39 +0,0 @@
1
- import { expect, test } from "@playwright/test";
2
- import { getAuthToken } from "@open-mercato/core/modules/core/__integration__/helpers/api";
3
- import {
4
- createCustomerData,
5
- createFixedTemplateInput,
6
- createLinkFixture,
7
- deleteCheckoutEntityIfExists,
8
- listCheckoutTransactions,
9
- submitPayLink
10
- } from "./helpers/fixtures.js";
11
- test.describe("TC-CHKT-042: Transaction search over encrypted customer data", () => {
12
- test("finds a newly created transaction through its token index", async ({ request }) => {
13
- const email = `token-search-${Date.now()}@example.test`;
14
- let token = null;
15
- let linkId = null;
16
- try {
17
- token = await getAuthToken(request);
18
- const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: "active" }));
19
- linkId = link.id;
20
- const submitted = await submitPayLink(request, link.slug, {
21
- customerData: createCustomerData({ email }),
22
- acceptedLegalConsents: {},
23
- amount: 49.99
24
- });
25
- expect(submitted.ok()).toBeTruthy();
26
- await expect.poll(async () => {
27
- const result = await listCheckoutTransactions(
28
- request,
29
- token,
30
- `search=${encodeURIComponent(email)}&page=1&pageSize=10`
31
- );
32
- return result.items.some((item) => item.email === email);
33
- }, { timeout: 15e3 }).toBeTruthy();
34
- } finally {
35
- await deleteCheckoutEntityIfExists(request, token, "links", linkId);
36
- }
37
- });
38
- });
39
- //# sourceMappingURL=TC-CHKT-042.spec.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../src/modules/checkout/__integration__/TC-CHKT-042.spec.ts"],
4
- "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport {\n createCustomerData,\n createFixedTemplateInput,\n createLinkFixture,\n deleteCheckoutEntityIfExists,\n listCheckoutTransactions,\n submitPayLink,\n} from './helpers/fixtures'\n\ntest.describe('TC-CHKT-042: Transaction search over encrypted customer data', () => {\n test('finds a newly created transaction through its token index', async ({ request }) => {\n const email = `token-search-${Date.now()}@example.test`\n let token: string | null = null\n let linkId: string | null = null\n\n try {\n token = await getAuthToken(request)\n const link = await createLinkFixture(request, token, createFixedTemplateInput({ status: 'active' }))\n linkId = link.id\n const submitted = await submitPayLink(request, link.slug, {\n customerData: createCustomerData({ email }),\n acceptedLegalConsents: {},\n amount: 49.99,\n })\n expect(submitted.ok()).toBeTruthy()\n\n await expect.poll(async () => {\n const result = await listCheckoutTransactions(\n request,\n token!,\n `search=${encodeURIComponent(email)}&page=1&pageSize=10`,\n )\n return result.items.some((item) => item.email === email)\n }, { timeout: 15_000 }).toBeTruthy()\n } finally {\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n})\n"],
5
- "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,KAAK,SAAS,gEAAgE,MAAM;AAClF,OAAK,6DAA6D,OAAO,EAAE,QAAQ,MAAM;AACvF,UAAM,QAAQ,gBAAgB,KAAK,IAAI,CAAC;AACxC,QAAI,QAAuB;AAC3B,QAAI,SAAwB;AAE5B,QAAI;AACF,cAAQ,MAAM,aAAa,OAAO;AAClC,YAAM,OAAO,MAAM,kBAAkB,SAAS,OAAO,yBAAyB,EAAE,QAAQ,SAAS,CAAC,CAAC;AACnG,eAAS,KAAK;AACd,YAAM,YAAY,MAAM,cAAc,SAAS,KAAK,MAAM;AAAA,QACxD,cAAc,mBAAmB,EAAE,MAAM,CAAC;AAAA,QAC1C,uBAAuB,CAAC;AAAA,QACxB,QAAQ;AAAA,MACV,CAAC;AACD,aAAO,UAAU,GAAG,CAAC,EAAE,WAAW;AAElC,YAAM,OAAO,KAAK,YAAY;AAC5B,cAAM,SAAS,MAAM;AAAA,UACnB;AAAA,UACA;AAAA,UACA,UAAU,mBAAmB,KAAK,CAAC;AAAA,QACrC;AACA,eAAO,OAAO,MAAM,KAAK,CAAC,SAAS,KAAK,UAAU,KAAK;AAAA,MACzD,GAAG,EAAE,SAAS,KAAO,CAAC,EAAE,WAAW;AAAA,IACrC,UAAE;AACA,YAAM,6BAA6B,SAAS,OAAO,SAAS,MAAM;AAAA,IACpE;AAAA,EACF,CAAC;AACH,CAAC;",
6
- "names": []
7
- }
@@ -1,62 +0,0 @@
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?.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