@endora-commerce/mod-payments 0.100.0

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/admin/index.d.ts +32 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +61 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/zones/OrderPaymentsTab.d.ts +42 -0
  8. package/dist/admin/zones/OrderPaymentsTab.d.ts.map +1 -0
  9. package/dist/admin/zones/OrderPaymentsTab.js +104 -0
  10. package/dist/admin/zones/OrderPaymentsTab.js.map +1 -0
  11. package/dist/backend/adapters/built-in-adapters.d.ts +58 -0
  12. package/dist/backend/adapters/built-in-adapters.d.ts.map +1 -0
  13. package/dist/backend/adapters/built-in-adapters.js +83 -0
  14. package/dist/backend/adapters/built-in-adapters.js.map +1 -0
  15. package/dist/backend/drivers/bank-transfer-driver.d.ts +28 -0
  16. package/dist/backend/drivers/bank-transfer-driver.d.ts.map +1 -0
  17. package/dist/backend/drivers/bank-transfer-driver.js +26 -0
  18. package/dist/backend/drivers/bank-transfer-driver.js.map +1 -0
  19. package/dist/backend/drivers/gateway-adapter-port.d.ts +71 -0
  20. package/dist/backend/drivers/gateway-adapter-port.d.ts.map +1 -0
  21. package/dist/backend/drivers/gateway-adapter-port.js +18 -0
  22. package/dist/backend/drivers/gateway-adapter-port.js.map +1 -0
  23. package/dist/backend/drivers/pickup-driver.d.ts +23 -0
  24. package/dist/backend/drivers/pickup-driver.d.ts.map +1 -0
  25. package/dist/backend/drivers/pickup-driver.js +19 -0
  26. package/dist/backend/drivers/pickup-driver.js.map +1 -0
  27. package/dist/backend/email-templates/transactional-defaults.d.ts +6 -0
  28. package/dist/backend/email-templates/transactional-defaults.d.ts.map +1 -0
  29. package/dist/backend/email-templates/transactional-defaults.js +27 -0
  30. package/dist/backend/email-templates/transactional-defaults.js.map +1 -0
  31. package/dist/backend/entities/payment.entity.d.ts +26 -0
  32. package/dist/backend/entities/payment.entity.d.ts.map +1 -0
  33. package/dist/backend/entities/payment.entity.js +100 -0
  34. package/dist/backend/entities/payment.entity.js.map +1 -0
  35. package/dist/backend/index.d.ts +88 -0
  36. package/dist/backend/index.d.ts.map +1 -0
  37. package/dist/backend/index.js +318 -0
  38. package/dist/backend/index.js.map +1 -0
  39. package/dist/backend/routes.customer.d.ts +20 -0
  40. package/dist/backend/routes.customer.d.ts.map +1 -0
  41. package/dist/backend/routes.customer.js +24 -0
  42. package/dist/backend/routes.customer.js.map +1 -0
  43. package/dist/backend/routes.d.ts +36 -0
  44. package/dist/backend/routes.d.ts.map +1 -0
  45. package/dist/backend/routes.js +44 -0
  46. package/dist/backend/routes.js.map +1 -0
  47. package/dist/backend/services/gateway-refund-registry.d.ts +122 -0
  48. package/dist/backend/services/gateway-refund-registry.d.ts.map +1 -0
  49. package/dist/backend/services/gateway-refund-registry.js +106 -0
  50. package/dist/backend/services/gateway-refund-registry.js.map +1 -0
  51. package/dist/backend/services/payment-email-notifier.d.ts +71 -0
  52. package/dist/backend/services/payment-email-notifier.d.ts.map +1 -0
  53. package/dist/backend/services/payment-email-notifier.js +73 -0
  54. package/dist/backend/services/payment-email-notifier.js.map +1 -0
  55. package/dist/backend/services/payment-email-renderer.d.ts +22 -0
  56. package/dist/backend/services/payment-email-renderer.d.ts.map +1 -0
  57. package/dist/backend/services/payment-email-renderer.js +17 -0
  58. package/dist/backend/services/payment-email-renderer.js.map +1 -0
  59. package/dist/backend/services/payment-placement-apply-port.d.ts +33 -0
  60. package/dist/backend/services/payment-placement-apply-port.d.ts.map +1 -0
  61. package/dist/backend/services/payment-placement-apply-port.js +68 -0
  62. package/dist/backend/services/payment-placement-apply-port.js.map +1 -0
  63. package/dist/backend/services/payment-read-port.d.ts +33 -0
  64. package/dist/backend/services/payment-read-port.d.ts.map +1 -0
  65. package/dist/backend/services/payment-read-port.js +64 -0
  66. package/dist/backend/services/payment-read-port.js.map +1 -0
  67. package/dist/backend/services/payment-reference-port.d.ts +29 -0
  68. package/dist/backend/services/payment-reference-port.d.ts.map +1 -0
  69. package/dist/backend/services/payment-reference-port.js +70 -0
  70. package/dist/backend/services/payment-reference-port.js.map +1 -0
  71. package/dist/backend/services/payment-refund.d.ts +55 -0
  72. package/dist/backend/services/payment-refund.d.ts.map +1 -0
  73. package/dist/backend/services/payment-refund.js +101 -0
  74. package/dist/backend/services/payment-refund.js.map +1 -0
  75. package/dist/backend/services/payment-retry-service.d.ts +45 -0
  76. package/dist/backend/services/payment-retry-service.d.ts.map +1 -0
  77. package/dist/backend/services/payment-retry-service.js +187 -0
  78. package/dist/backend/services/payment-retry-service.js.map +1 -0
  79. package/dist/backend/services/payment-service.d.ts +62 -0
  80. package/dist/backend/services/payment-service.d.ts.map +1 -0
  81. package/dist/backend/services/payment-service.js +134 -0
  82. package/dist/backend/services/payment-service.js.map +1 -0
  83. package/dist/backend/services/receive-payment-handler.d.ts +260 -0
  84. package/dist/backend/services/receive-payment-handler.d.ts.map +1 -0
  85. package/dist/backend/services/receive-payment-handler.js +356 -0
  86. package/dist/backend/services/receive-payment-handler.js.map +1 -0
  87. package/dist/backend/services/registry-singleton.d.ts +16 -0
  88. package/dist/backend/services/registry-singleton.d.ts.map +1 -0
  89. package/dist/backend/services/registry-singleton.js +17 -0
  90. package/dist/backend/services/registry-singleton.js.map +1 -0
  91. package/dist/manifest.d.ts +210 -0
  92. package/dist/manifest.d.ts.map +1 -0
  93. package/dist/manifest.js +182 -0
  94. package/dist/manifest.js.map +1 -0
  95. package/dist/migrations/20260925T115728_payments_refunded_amount.d.ts +28 -0
  96. package/dist/migrations/20260925T115728_payments_refunded_amount.d.ts.map +1 -0
  97. package/dist/migrations/20260925T115728_payments_refunded_amount.js +32 -0
  98. package/dist/migrations/20260925T115728_payments_refunded_amount.js.map +1 -0
  99. package/dist/migrations/index.d.ts +27 -0
  100. package/dist/migrations/index.d.ts.map +1 -0
  101. package/dist/migrations/index.js +29 -0
  102. package/dist/migrations/index.js.map +1 -0
  103. package/docs/payments.md +69 -0
  104. package/i18n/en.json +5 -0
  105. package/i18n/pl.json +5 -0
  106. package/package.json +96 -0
  107. package/tailwind.css +14 -0
@@ -0,0 +1,70 @@
1
+ import { Payment } from '../entities/payment.entity.js';
2
+ /**
3
+ * The one write the four payment gateways make into this module's table
4
+ * (feature 075, Phase P) — the write side of
5
+ * `PaymentReadPort.findByExternalReference`.
6
+ *
7
+ * Each gateway opens its own provider object for an attempt (a Stripe
8
+ * PaymentIntent or Checkout Session, a TPay transaction, a PayU order, an
9
+ * Autopay transaction) and then records that object's identifier on the
10
+ * attempt, so a later provider event carrying only the provider's reference
11
+ * resolves back to a payment. Four modules were doing it with
12
+ * `em.findOne(Payment, …)` and a field assignment, which is this module's row
13
+ * being written by somebody else's `EntityManager`.
14
+ *
15
+ * It is deliberately not on the Command Bus, and the four call sites all say
16
+ * why in their own words: this is provider-integration bookkeeping, not an
17
+ * operator decision. The state change an operator is answerable for is the
18
+ * settlement, and that is audited in the payments/orders flow.
19
+ */
20
+ export class PaymentReferenceService {
21
+ emFactory;
22
+ constructor(emFactory) {
23
+ this.emFactory = emFactory;
24
+ }
25
+ async stampExternalReference(paymentId, externalReference) {
26
+ // Records the provider's own identifier for an attempt so its later event
27
+ // resolves; provider-integration bookkeeping. The exemption sits on `stamp`,
28
+ // which is where the write is — a second marker here guarded nothing, and
29
+ // since D-89(c) the staleness half says so instead of counting it.
30
+ return this.stamp(paymentId, externalReference, false);
31
+ }
32
+ async stampExternalReferenceIfAbsent(paymentId, externalReference) {
33
+ // As above, for the gateway whose first reference is the one its provider
34
+ // metadata was written against; `stamp` carries the exemption.
35
+ return this.stamp(paymentId, externalReference, true);
36
+ }
37
+ async mergeProviderDetails(paymentId, patch) {
38
+ // command-coverage-ignore: records what the provider said about an attempt
39
+ // so a later provider event can act on it; provider-integration
40
+ // bookkeeping, exactly as the reference stamp above.
41
+ const em = this.emFactory();
42
+ const payment = await em.findOne(Payment, { id: paymentId });
43
+ if (!payment)
44
+ return false;
45
+ payment.providerDetails = { ...(payment.providerDetails ?? {}), ...patch };
46
+ await em.flush();
47
+ return true;
48
+ }
49
+ async stamp(paymentId, externalReference, onlyIfAbsent) {
50
+ // command-coverage-ignore: the mutation both public entry points delegate
51
+ // to, and the one place the exemption belongs — recording the provider's own
52
+ // identifier for an attempt is integration bookkeeping, not an operator
53
+ // decision. Marking only the callers left `--strict` blocking on the line
54
+ // below (feature 075, Phase P); marking the callers *as well* left two
55
+ // markers guarding nothing, which is what D-89(c) taught the staleness half
56
+ // to report.
57
+ const em = this.emFactory();
58
+ const payment = await em.findOne(Payment, { id: paymentId });
59
+ if (!payment)
60
+ return false;
61
+ if (onlyIfAbsent && payment.externalReference)
62
+ return false;
63
+ if (payment.externalReference === externalReference)
64
+ return false;
65
+ payment.externalReference = externalReference;
66
+ await em.flush();
67
+ return true;
68
+ }
69
+ }
70
+ //# sourceMappingURL=payment-reference-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-reference-port.js","sourceRoot":"","sources":["../../../src/backend/services/payment-reference-port.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAExD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,uBAAuB;IACL;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,sBAAsB,CAAC,SAAiB,EAAE,iBAAyB;QACvE,0EAA0E;QAC1E,6EAA6E;QAC7E,0EAA0E;QAC1E,mEAAmE;QACnE,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,iBAAiB,EAAE,KAAK,CAAC,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,8BAA8B,CAClC,SAAiB,EACjB,iBAAyB;QAEzB,0EAA0E;QAC1E,+DAA+D;QAC/D,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,iBAAiB,EAAE,IAAI,CAAC,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,oBAAoB,CACxB,SAAiB,EACjB,KAA8B;QAE9B,2EAA2E;QAC3E,gEAAgE;QAChE,qDAAqD;QACrD,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAC3B,OAAO,CAAC,eAAe,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,eAAe,IAAI,EAAE,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC;QAC3E,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,KAAK,CAAC,KAAK,CACjB,SAAiB,EACjB,iBAAyB,EACzB,YAAqB;QAErB,0EAA0E;QAC1E,6EAA6E;QAC7E,wEAAwE;QACxE,0EAA0E;QAC1E,uEAAuE;QACvE,4EAA4E;QAC5E,aAAa;QACb,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAC3B,IAAI,YAAY,IAAI,OAAO,CAAC,iBAAiB;YAAE,OAAO,KAAK,CAAC;QAC5D,IAAI,OAAO,CAAC,iBAAiB,KAAK,iBAAiB;YAAE,OAAO,KAAK,CAAC;QAClE,OAAO,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;QAC9C,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
@@ -0,0 +1,55 @@
1
+ import type { OrderReadPort, PaymentMethodReadPort, PaymentRefundInput, PaymentRefundPort, PaymentRefundResult } from '@endora-commerce/contracts';
2
+ /**
3
+ * Payments-side implementation of the returns module's `PaymentRefundPort`
4
+ * (feature 046, R5).
5
+ *
6
+ * Gateway payments (`kind === 'gateway'`) are delegated to the PSP handler
7
+ * registered under the **order's** payment adapter (snapshot / order method).
8
+ * The settlement form's `refundPaymentMethodId` is ledger metadata only — it
9
+ * must not pick which PSP to call (otherwise Autopay orders refunded under a
10
+ * bank-transfer method never hit Autopay).
11
+ *
12
+ * Offline methods (bank transfer, credit limit, pickup) are recorded as
13
+ * `issued` — the operator performs the actual transfer.
14
+ *
15
+ * **A gateway the operator switched off is refused, not recorded (D-71).**
16
+ * `gatewayRefundRegistry` skips an absent owner's handler, and this provider
17
+ * used to answer the resulting gap with `pending_manual`. Every layer above
18
+ * reads that as *settled*: `ReturnSettlementService` goes on to resolve the
19
+ * case, write the `Refund` row, issue the corrective invoice and mail the
20
+ * customer, and the admin's settlement card branches on `'failed'` only, so the
21
+ * operator is told "Settled." while no money has moved and the PSP was never
22
+ * called. Nothing in the execution path was missing a presence probe — the
23
+ * probe was right and the *outcome* was wrong.
24
+ *
25
+ * So the refusal is a {@link ModuleDisabledError}: the same 503 envelope, with
26
+ * the same `Retry-After`, that any other call into a switched-off module
27
+ * produces. It leaves the case exactly where it was, which is the truthful
28
+ * state, and it names the module — switching it back on is the whole remedy.
29
+ *
30
+ * `pending_manual` stays, and keeping the two apart is the point: a deployment
31
+ * that never installed a PSP integration is not in a temporary state and has
32
+ * nothing to switch on, so its refunds belong on the platform's books for a
33
+ * person to settle.
34
+ */
35
+ export declare class PaymentRefundProvider implements PaymentRefundPort {
36
+ private readonly orderRead;
37
+ private readonly paymentMethodRead;
38
+ /**
39
+ * Feature 075 Phase C — both reads are somebody else's, both are reads, and
40
+ * neither is inside a transaction this class opens: the order comes from
41
+ * `orderReadPort` and the payment method from `paymentMethodReadPort`. They
42
+ * fail closed, which is the same answer D-71 already reaches for a
43
+ * switched-off gateway and for the same reason — resolving a refund against
44
+ * data the platform will not read is worse than refusing it.
45
+ */
46
+ constructor(orderRead: OrderReadPort, paymentMethodRead: PaymentMethodReadPort);
47
+ refund(input: PaymentRefundInput): Promise<PaymentRefundResult>;
48
+ /**
49
+ * Prefer the adapter the order was placed with (snapshot), then the order's
50
+ * payment method row. Do not use the settlement refund-method id for PSP
51
+ * resolution when the order snapshot already names an adapter.
52
+ */
53
+ private resolveOrderAdapterKey;
54
+ }
55
+ //# sourceMappingURL=payment-refund.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-refund.d.ts","sourceRoot":"","sources":["../../../src/backend/services/payment-refund.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EAEb,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACpB,MAAM,4BAA4B,CAAC;AAIpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,qBAAsB,YAAW,iBAAiB;IAU3D,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAVpC;;;;;;;OAOG;gBAEgB,SAAS,EAAE,aAAa,EACxB,iBAAiB,EAAE,qBAAqB;IAGrD,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA4BrE;;;;OAIG;YACW,sBAAsB;CAmBrC"}
@@ -0,0 +1,101 @@
1
+ import { ModuleDisabledError } from '@endora-commerce/platform/kernel';
2
+ import { gatewayRefundRegistry } from './registry-singleton.js';
3
+ /**
4
+ * Payments-side implementation of the returns module's `PaymentRefundPort`
5
+ * (feature 046, R5).
6
+ *
7
+ * Gateway payments (`kind === 'gateway'`) are delegated to the PSP handler
8
+ * registered under the **order's** payment adapter (snapshot / order method).
9
+ * The settlement form's `refundPaymentMethodId` is ledger metadata only — it
10
+ * must not pick which PSP to call (otherwise Autopay orders refunded under a
11
+ * bank-transfer method never hit Autopay).
12
+ *
13
+ * Offline methods (bank transfer, credit limit, pickup) are recorded as
14
+ * `issued` — the operator performs the actual transfer.
15
+ *
16
+ * **A gateway the operator switched off is refused, not recorded (D-71).**
17
+ * `gatewayRefundRegistry` skips an absent owner's handler, and this provider
18
+ * used to answer the resulting gap with `pending_manual`. Every layer above
19
+ * reads that as *settled*: `ReturnSettlementService` goes on to resolve the
20
+ * case, write the `Refund` row, issue the corrective invoice and mail the
21
+ * customer, and the admin's settlement card branches on `'failed'` only, so the
22
+ * operator is told "Settled." while no money has moved and the PSP was never
23
+ * called. Nothing in the execution path was missing a presence probe — the
24
+ * probe was right and the *outcome* was wrong.
25
+ *
26
+ * So the refusal is a {@link ModuleDisabledError}: the same 503 envelope, with
27
+ * the same `Retry-After`, that any other call into a switched-off module
28
+ * produces. It leaves the case exactly where it was, which is the truthful
29
+ * state, and it names the module — switching it back on is the whole remedy.
30
+ *
31
+ * `pending_manual` stays, and keeping the two apart is the point: a deployment
32
+ * that never installed a PSP integration is not in a temporary state and has
33
+ * nothing to switch on, so its refunds belong on the platform's books for a
34
+ * person to settle.
35
+ */
36
+ export class PaymentRefundProvider {
37
+ orderRead;
38
+ paymentMethodRead;
39
+ /**
40
+ * Feature 075 Phase C — both reads are somebody else's, both are reads, and
41
+ * neither is inside a transaction this class opens: the order comes from
42
+ * `orderReadPort` and the payment method from `paymentMethodReadPort`. They
43
+ * fail closed, which is the same answer D-71 already reaches for a
44
+ * switched-off gateway and for the same reason — resolving a refund against
45
+ * data the platform will not read is worse than refusing it.
46
+ */
47
+ constructor(orderRead, paymentMethodRead) {
48
+ this.orderRead = orderRead;
49
+ this.paymentMethodRead = paymentMethodRead;
50
+ }
51
+ async refund(input) {
52
+ const order = await this.orderRead.findById(input.orderId);
53
+ const kind = order?.paymentMethodSnapshot?.kind;
54
+ if (kind === 'gateway') {
55
+ const adapterKey = await this.resolveOrderAdapterKey(order, input.paymentMethodId);
56
+ const handler = gatewayRefundRegistry.resolve(adapterKey);
57
+ if (handler) {
58
+ return handler.refund(input);
59
+ }
60
+ // Two different situations behind one empty `resolve`, and only one of
61
+ // them is an outcome. A gateway whose module is switched off is a
62
+ // capability that is supposed to be here: refuse, so nothing downstream
63
+ // resolves the case, issues a correction or mails the customer over a
64
+ // refund that did not happen (D-71). `absentOwnerFor` is presence-blind
65
+ // for exactly this — it still names the contributor.
66
+ const absentOwner = gatewayRefundRegistry.absentOwnerFor(adapterKey);
67
+ if (absentOwner !== null)
68
+ throw new ModuleDisabledError(absentOwner);
69
+ // Nobody ever registered a handler for this adapter: there is no module
70
+ // to switch on, so the refund goes on the platform's books, named, for a
71
+ // person to settle.
72
+ return {
73
+ state: 'pending_manual',
74
+ failureReason: 'Gateway refunds require a PSP refund integration.',
75
+ };
76
+ }
77
+ return { state: 'issued', externalReference: input.idempotencyKey };
78
+ }
79
+ /**
80
+ * Prefer the adapter the order was placed with (snapshot), then the order's
81
+ * payment method row. Do not use the settlement refund-method id for PSP
82
+ * resolution when the order snapshot already names an adapter.
83
+ */
84
+ async resolveOrderAdapterKey(order, settlementPaymentMethodId) {
85
+ const fromSnapshot = order?.paymentMethodSnapshot?.adapter?.trim();
86
+ if (fromSnapshot)
87
+ return fromSnapshot;
88
+ if (order?.paymentMethodId) {
89
+ const method = await this.paymentMethodRead.findById(order.paymentMethodId);
90
+ if (method?.adapter)
91
+ return method.adapter;
92
+ }
93
+ // Last resort: settlement form method (legacy callers / missing snapshot).
94
+ if (settlementPaymentMethodId) {
95
+ const method = await this.paymentMethodRead.findById(settlementPaymentMethodId);
96
+ return method?.adapter ?? null;
97
+ }
98
+ return null;
99
+ }
100
+ }
101
+ //# sourceMappingURL=payment-refund.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-refund.js","sourceRoot":"","sources":["../../../src/backend/services/payment-refund.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,OAAO,qBAAqB;IAUb;IACA;IAVnB;;;;;;;OAOG;IACH,YACmB,SAAwB,EACxB,iBAAwC;QADxC,cAAS,GAAT,SAAS,CAAe;QACxB,sBAAiB,GAAjB,iBAAiB,CAAuB;IACxD,CAAC;IAEJ,KAAK,CAAC,MAAM,CAAC,KAAyB;QACpC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,KAAK,EAAE,qBAAqB,EAAE,IAAI,CAAC;QAChD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,sBAAsB,CAAC,KAAK,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;YACnF,MAAM,OAAO,GAAG,qBAAqB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;YAC1D,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC/B,CAAC;YACD,uEAAuE;YACvE,kEAAkE;YAClE,wEAAwE;YACxE,sEAAsE;YACtE,wEAAwE;YACxE,qDAAqD;YACrD,MAAM,WAAW,GAAG,qBAAqB,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;YACrE,IAAI,WAAW,KAAK,IAAI;gBAAE,MAAM,IAAI,mBAAmB,CAAC,WAAW,CAAC,CAAC;YACrE,wEAAwE;YACxE,yEAAyE;YACzE,oBAAoB;YACpB,OAAO;gBACL,KAAK,EAAE,gBAAgB;gBACvB,aAAa,EAAE,mDAAmD;aACnE,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,KAAK,CAAC,cAAc,EAAE,CAAC;IACtE,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,sBAAsB,CAClC,KAAyB,EACzB,yBAA6C;QAE7C,MAAM,YAAY,GAAG,KAAK,EAAE,qBAAqB,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACnE,IAAI,YAAY;YAAE,OAAO,YAAY,CAAC;QAEtC,IAAI,KAAK,EAAE,eAAe,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;YAC5E,IAAI,MAAM,EAAE,OAAO;gBAAE,OAAO,MAAM,CAAC,OAAO,CAAC;QAC7C,CAAC;QAED,2EAA2E;QAC3E,IAAI,yBAAyB,EAAE,CAAC;YAC9B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC;YAChF,OAAO,MAAM,EAAE,OAAO,IAAI,IAAI,CAAC;QACjC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
@@ -0,0 +1,45 @@
1
+ import { type CustomerAccountReadPort, type OrderReadPort, type OrderTransitionPort, type PaymentAdapterRegistryPort, type PaymentRetryResult } from '@endora-commerce/contracts';
2
+ import type { PaymentService } from './payment-service.js';
3
+ export interface PaymentRetryDeps {
4
+ orderRead: OrderReadPort;
5
+ customerAccountRead: CustomerAccountReadPort;
6
+ paymentService: PaymentService;
7
+ /**
8
+ * The lifecycle read, for the terminality term. `orders` owns the graph and
9
+ * the graph is what decides which statuses are ends — this module may not
10
+ * name one.
11
+ */
12
+ orderTransition: OrderTransitionPort;
13
+ /**
14
+ * Read per call rather than captured: the registry is contributed at boot by
15
+ * whichever gateway modules are present, and an operator can switch one off
16
+ * between two requests.
17
+ */
18
+ paymentAdapterRegistry: () => PaymentAdapterRegistryPort;
19
+ assertOrganizationCanTransact: (organizationId: string) => Promise<void>;
20
+ }
21
+ export declare class PaymentRetryService {
22
+ private readonly deps;
23
+ constructor(deps: PaymentRetryDeps);
24
+ /**
25
+ * Open (or resume) the buyer's own next payment attempt for `orderId`.
26
+ *
27
+ * @throws 404 when the order does not exist or carries no payment attempt.
28
+ * @throws 403 when the caller did not place the order.
29
+ * @throws 409 `PAYMENT_NOT_DUE` when the money is not the buyer's to pay —
30
+ * the order is paid, drawn against a credit limit, or refunded.
31
+ * @throws 409 `PAYMENT_ORDER_CLOSED` when the order's lifecycle status is
32
+ * terminal. A different condition and so a different code: the money
33
+ * may well still be owed, but the order it was owed on is over.
34
+ * @throws 409 `PAYMENT_ADAPTER_UNAVAILABLE` when the method the order was
35
+ * placed with has no adapter registered any more. A third condition,
36
+ * and the only one that is about the shop rather than the order.
37
+ * @throws whatever `assertOrganizationCanTransact` throws for a suspended or
38
+ * blocked organisation — the route maps it, exactly as placement does.
39
+ */
40
+ retryForCustomer(input: {
41
+ orderId: string;
42
+ customerAccountId: string;
43
+ }): Promise<PaymentRetryResult>;
44
+ }
45
+ //# sourceMappingURL=payment-retry-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-retry-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/payment-retry-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,uBAAuB,EAE5B,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAE/B,KAAK,kBAAkB,EACxB,MAAM,4BAA4B,CAAC;AAIpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA6D3D,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,aAAa,CAAC;IACzB,mBAAmB,EAAE,uBAAuB,CAAC;IAC7C,cAAc,EAAE,cAAc,CAAC;IAC/B;;;;OAIG;IACH,eAAe,EAAE,mBAAmB,CAAC;IACrC;;;;OAIG;IACH,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;IACzD,6BAA6B,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E;AAED,qBAAa,mBAAmB;IAClB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,gBAAgB;IAEnD;;;;;;;;;;;;;;;OAeG;IACG,gBAAgB,CAAC,KAAK,EAAE;QAC5B,OAAO,EAAE,MAAM,CAAC;QAChB,iBAAiB,EAAE,MAAM,CAAC;KAC3B,GAAG,OAAO,CAAC,kBAAkB,CAAC;CA6HhC"}
@@ -0,0 +1,187 @@
1
+ import { ERROR_CODES, } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { rethrowIfModuleDisabled } from '@endora-commerce/platform/kernel';
4
+ import { paymentsErrorCodes } from '../../manifest.js';
5
+ /**
6
+ * A buyer paying an order of theirs again after the first attempt did not go
7
+ * through (issue #264).
8
+ *
9
+ * Before this existed there was no supported path at all. `/checkout/pay?id=…`
10
+ * is linked from the checkout submit action and from nowhere else, so a buyer
11
+ * whose card was declined had the page in their history or nothing; and going
12
+ * back through `/checkout` would have placed a **second** order for goods the
13
+ * first one still has allocated. The four redirect-mode gateways had not even
14
+ * that: the gateway URL is returned once, in the place-order response's
15
+ * `nextAction`, which is a virtual column and is never persisted.
16
+ *
17
+ * ### What this decides on, and what it deliberately does not
18
+ *
19
+ * The refusals below take **two** terms: the payment axis — is this order paid,
20
+ * is there an attempt, has it failed — and, since feature 085's Phase D,
21
+ * whether the order's lifecycle status is terminal.
22
+ *
23
+ * Reading the lifecycle at all used to be unsafe: every payment method in the
24
+ * tree was seeded
25
+ * `status_on_failure = 'cancelled'`, so the settlement ingress made an order
26
+ * terminal on the first decline and reading `order.status` would have refused
27
+ * exactly the buyers this exists for. Feature 085 answered the product question
28
+ * behind it — a declined payment now holds the order at the method's failure
29
+ * status, seeded `on_hold`, and records the decline on the money axis as
30
+ * `paymentStatus = 'failed'`. So a declined order is no longer terminal, and
31
+ * the cost this doc block used to state — that a hand-cancelled order was
32
+ * indistinguishable from an ingress-cancelled one — is retired with it: the
33
+ * ingress cancels nothing.
34
+ *
35
+ * The money term is an **allow-list of two**, `awaiting_payment` and `failed`,
36
+ * and not a negation of `paid`. `deferred` is a credit-limit order whose credit
37
+ * was drawn inside the placement transaction — the shop is already acting on
38
+ * it, and there is no buyer-initiated session to open — and `refunded` is
39
+ * settled in the other direction.
40
+ *
41
+ * The lifecycle term is **terminality, not the string `cancelled`**. The status
42
+ * set is operator-configurable and a deployment may add terminal statuses of
43
+ * its own, so the question goes to the graph through
44
+ * `orderTransitionPort.isTerminal`. And the reason it is asked at all is Phase
45
+ * D's: the ingress no longer writes a terminal status, so a terminal order is
46
+ * always a deliberate human decision — an administrator's, or the buyer's own —
47
+ * and paying it again would silently override the person who made it, on stock
48
+ * the cancellation has already released.
49
+ */
50
+ /**
51
+ * The payment states in which the buyer still owes this money themselves
52
+ * (feature 085, R13's money term).
53
+ *
54
+ * An allow-list rather than `!== 'paid'`: `deferred` is a credit-limit order,
55
+ * unpaid by arrangement and already drawn, and `refunded` is settled the other
56
+ * way. Both would pass a negation and neither has a session for the buyer to
57
+ * open.
58
+ */
59
+ const BUYER_STILL_OWES = new Set([
60
+ 'awaiting_payment',
61
+ 'failed',
62
+ ]);
63
+ export class PaymentRetryService {
64
+ deps;
65
+ constructor(deps) {
66
+ this.deps = deps;
67
+ }
68
+ /**
69
+ * Open (or resume) the buyer's own next payment attempt for `orderId`.
70
+ *
71
+ * @throws 404 when the order does not exist or carries no payment attempt.
72
+ * @throws 403 when the caller did not place the order.
73
+ * @throws 409 `PAYMENT_NOT_DUE` when the money is not the buyer's to pay —
74
+ * the order is paid, drawn against a credit limit, or refunded.
75
+ * @throws 409 `PAYMENT_ORDER_CLOSED` when the order's lifecycle status is
76
+ * terminal. A different condition and so a different code: the money
77
+ * may well still be owed, but the order it was owed on is over.
78
+ * @throws 409 `PAYMENT_ADAPTER_UNAVAILABLE` when the method the order was
79
+ * placed with has no adapter registered any more. A third condition,
80
+ * and the only one that is about the shop rather than the order.
81
+ * @throws whatever `assertOrganizationCanTransact` throws for a suspended or
82
+ * blocked organisation — the route maps it, exactly as placement does.
83
+ */
84
+ async retryForCustomer(input) {
85
+ const order = await this.deps.orderRead.findById(input.orderId);
86
+ if (!order) {
87
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Order not found.');
88
+ }
89
+ // Ownership before anything else: keyed on an order id, this route is an
90
+ // enumeration surface until it has answered "is this yours?".
91
+ if (order.placedByCustomerAccountId !== input.customerAccountId) {
92
+ throw new HttpError(403, ERROR_CODES.FORBIDDEN, 'This order does not belong to you.');
93
+ }
94
+ if (!BUYER_STILL_OWES.has(order.paymentStatus)) {
95
+ // The money term, and one code for all three of the statuses it refuses
96
+ // (`paid`, `deferred`, `refunded`): one predicate, one `throw`, one
97
+ // sentence the buyer reads either way.
98
+ //
99
+ // It answered `VALIDATION_FAILED`, which is worse than an untidy code —
100
+ // that is the one code `localizeErrorEnvelope` returns *before*
101
+ // translating (`@endora-commerce/platform/http`), because the code is
102
+ // overloaded and several services carry machine-readable tokens in its
103
+ // message. So a buyer read the English written here whatever language
104
+ // they asked for. The message below is now the raise-site fallback the
105
+ // envelope substitutes, reached only when no bundle answers.
106
+ throw new HttpError(409, paymentsErrorCodes.PAYMENT_NOT_DUE, 'This order is not awaiting payment.');
107
+ }
108
+ // The lifecycle term (feature 085 Phase D). Asked of the graph, because the
109
+ // terminal set is operator-configurable — and asked after the money term so
110
+ // the two refusals stay in the order the buyer's own page explains them in.
111
+ // `null` is "no such order", which the read above says otherwise; it means
112
+ // the order was deleted between the two reads, and the attempt this would
113
+ // open is the one that then answers.
114
+ if ((await this.deps.orderTransition.isTerminal(order.id)) === true) {
115
+ // A second code rather than the money term's, because the two terms are
116
+ // orthogonal and so are the buyer's answers to them: `PAYMENT_NOT_DUE`
117
+ // says there is nothing to pay, this says the order is over and the
118
+ // stock its cancellation released is somebody else's now. A buyer who
119
+ // still wants the goods places a new order; a buyer told the other
120
+ // sentence does nothing at all.
121
+ throw new HttpError(409, paymentsErrorCodes.PAYMENT_ORDER_CLOSED, 'This order is closed and can no longer be paid.');
122
+ }
123
+ await this.deps.assertOrganizationCanTransact(order.organizationId);
124
+ const { payment, opened } = await this.deps.paymentService.openRetry(order.id);
125
+ if (!opened) {
126
+ // An attempt is already open. Contacting the provider again would be a
127
+ // second object against one attempt — see `PaymentRetryResult`.
128
+ return {
129
+ paymentId: payment.id,
130
+ attemptNo: payment.attemptNo,
131
+ opened: false,
132
+ nextAction: { kind: 'none' },
133
+ };
134
+ }
135
+ const adapterKey = order.paymentMethodSnapshot.adapter;
136
+ const adapter = adapterKey ? this.deps.paymentAdapterRegistry().get(adapterKey) : undefined;
137
+ if (!adapter) {
138
+ await this.deps.paymentService.failAttempt(payment.id, 'No payment adapter is available for this order.');
139
+ // The third code, and the one refusal here that is not about the order:
140
+ // it is open, the money is still owed, and the platform cannot start a
141
+ // session because the adapter the method names is not registered any
142
+ // more. The buyer's move is to ask the shop, and the shop's is to repair
143
+ // a configuration — neither of the other two sentences says that.
144
+ throw new HttpError(409, paymentsErrorCodes.PAYMENT_ADAPTER_UNAVAILABLE, 'The payment method this order was placed with is no longer available.');
145
+ }
146
+ const buyer = await this.deps.customerAccountRead.findById(order.placedByCustomerAccountId);
147
+ const payerName = [buyer?.firstName, buyer?.lastName].filter(Boolean).join(' ').trim() ||
148
+ order.billingAddress.recipientName ||
149
+ buyer?.email ||
150
+ null;
151
+ let started;
152
+ try {
153
+ const result = await adapter.onStorefrontOrderCreated({
154
+ orderId: order.id,
155
+ paymentId: payment.id,
156
+ amount: Number(payment.amount),
157
+ currency: payment.currency,
158
+ paymentMethodCode: order.paymentMethodSnapshot.code,
159
+ paymentMethodId: order.paymentMethodId,
160
+ salesChannelId: order.salesChannelId,
161
+ payerEmail: buyer?.email ?? null,
162
+ payerName,
163
+ billingCountry: order.billingAddress.country ?? null,
164
+ orderBusinessId: order.businessId,
165
+ });
166
+ started = result;
167
+ }
168
+ catch (error) {
169
+ // A narrow, compensating tolerance, and the one shape the checklist
170
+ // endorses: the attempt row is written before the adapter can be asked
171
+ // (it needs the id), so an adapter that refuses must not leave an open
172
+ // attempt no provider knows about — the buyer's next click would resume
173
+ // that phantom instead of opening a real attempt. The failure is
174
+ // re-thrown; only the row is repaired.
175
+ rethrowIfModuleDisabled(error);
176
+ await this.deps.paymentService.failAttempt(payment.id, error instanceof Error ? error.message : 'The payment could not be started.');
177
+ throw error;
178
+ }
179
+ return {
180
+ paymentId: payment.id,
181
+ attemptNo: payment.attemptNo,
182
+ opened: true,
183
+ nextAction: started,
184
+ };
185
+ }
186
+ }
187
+ //# sourceMappingURL=payment-retry-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-retry-service.js","sourceRoot":"","sources":["../../../src/backend/services/payment-retry-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type OrderReadPort } from '@endora-commerce/contracts';
3
+ import { Payment } from '../entities/payment.entity.js';
4
+ /**
5
+ * The two things "open a retry" can mean, kept apart because they are the
6
+ * difference between contacting a provider and not (issue #264).
7
+ *
8
+ * `opened: true` is a new attempt row — the previous one had failed, so the
9
+ * next `attemptNo` is the record a fresh provider session will settle.
10
+ * `opened: false` is the attempt that was already open being handed back
11
+ * unchanged: a second click, a reload, a buyer returning to a payment they
12
+ * walked away from. The caller must not start a provider session for it.
13
+ */
14
+ export interface OpenRetryResult {
15
+ payment: Payment;
16
+ opened: boolean;
17
+ }
18
+ /**
19
+ * PaymentService (feature 034, FR-024) — opens retry Payments. The first
20
+ * Payment of an order is created by OrderService.placeOrder; a retry opens a
21
+ * new row against the same order with the next `attemptNo`, leaving prior
22
+ * (failed) attempts intact so the admin can see the full history.
23
+ *
24
+ * **A fresh attempt row is what makes a gateway able to start over**
25
+ * (issue #264), which is why the customer-facing retry runs through here rather
26
+ * than re-driving the failed row. Every gateway keys its own object on this
27
+ * platform's payment id: PayU sends it as `extOrderId`, which must be unique
28
+ * per POS; TPay resolves the newest `tpay_transactions` row for the order;
29
+ * Stripe records a `stripe_payment_intents` mapping per payment. Re-using the
30
+ * failed attempt asks all four to open a second session against an identifier
31
+ * they have already spent.
32
+ */
33
+ export declare class PaymentService {
34
+ #private;
35
+ private readonly emFactory;
36
+ private readonly orderRead;
37
+ constructor(emFactory: () => EntityManager, orderRead: OrderReadPort);
38
+ /**
39
+ * The next attempt for `orderId`, opening one only when the latest has
40
+ * failed.
41
+ *
42
+ * The latest attempt is read `FOR UPDATE`, so two callers racing on the same
43
+ * order serialise here and the second sees what the first wrote: a buyer
44
+ * double-clicking gets one new attempt, not two. It is the same row the
45
+ * settlement ingress resolves, so a retry and a late gateway callback cannot
46
+ * interleave halfway through each other.
47
+ */
48
+ openRetry(orderId: string): Promise<OpenRetryResult>;
49
+ /**
50
+ * Close an attempt whose provider session could not be started.
51
+ *
52
+ * The compensating half of {@link openRetry}: the row is written before the
53
+ * adapter is asked (the adapter needs its id), so an adapter that throws
54
+ * would otherwise leave an `awaiting_payment` attempt no provider knows
55
+ * about — and the next retry would *resume* that phantom instead of opening a
56
+ * real one. Marking it failed puts the order back where the buyer can try
57
+ * again.
58
+ */
59
+ failAttempt(paymentId: string, reason: string): Promise<void>;
60
+ listForOrder(orderId: string): Promise<Payment[]>;
61
+ }
62
+ //# sourceMappingURL=payment-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/payment-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE7E,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAExD;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,cAAc;;IAEvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,SAAS,EAAE,MAAM,aAAa,EAC9B,SAAS,EAAE,aAAa;IAkC3C;;;;;;;;;OASG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAkD1D;;;;;;;;;OASG;IACG,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc7D,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CAKxD"}