@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.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/admin/index.d.ts +32 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +61 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/zones/OrderPaymentsTab.d.ts +42 -0
- package/dist/admin/zones/OrderPaymentsTab.d.ts.map +1 -0
- package/dist/admin/zones/OrderPaymentsTab.js +104 -0
- package/dist/admin/zones/OrderPaymentsTab.js.map +1 -0
- package/dist/backend/adapters/built-in-adapters.d.ts +58 -0
- package/dist/backend/adapters/built-in-adapters.d.ts.map +1 -0
- package/dist/backend/adapters/built-in-adapters.js +83 -0
- package/dist/backend/adapters/built-in-adapters.js.map +1 -0
- package/dist/backend/drivers/bank-transfer-driver.d.ts +28 -0
- package/dist/backend/drivers/bank-transfer-driver.d.ts.map +1 -0
- package/dist/backend/drivers/bank-transfer-driver.js +26 -0
- package/dist/backend/drivers/bank-transfer-driver.js.map +1 -0
- package/dist/backend/drivers/gateway-adapter-port.d.ts +71 -0
- package/dist/backend/drivers/gateway-adapter-port.d.ts.map +1 -0
- package/dist/backend/drivers/gateway-adapter-port.js +18 -0
- package/dist/backend/drivers/gateway-adapter-port.js.map +1 -0
- package/dist/backend/drivers/pickup-driver.d.ts +23 -0
- package/dist/backend/drivers/pickup-driver.d.ts.map +1 -0
- package/dist/backend/drivers/pickup-driver.js +19 -0
- package/dist/backend/drivers/pickup-driver.js.map +1 -0
- package/dist/backend/email-templates/transactional-defaults.d.ts +6 -0
- package/dist/backend/email-templates/transactional-defaults.d.ts.map +1 -0
- package/dist/backend/email-templates/transactional-defaults.js +27 -0
- package/dist/backend/email-templates/transactional-defaults.js.map +1 -0
- package/dist/backend/entities/payment.entity.d.ts +26 -0
- package/dist/backend/entities/payment.entity.d.ts.map +1 -0
- package/dist/backend/entities/payment.entity.js +100 -0
- package/dist/backend/entities/payment.entity.js.map +1 -0
- package/dist/backend/index.d.ts +88 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +318 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.customer.d.ts +20 -0
- package/dist/backend/routes.customer.d.ts.map +1 -0
- package/dist/backend/routes.customer.js +24 -0
- package/dist/backend/routes.customer.js.map +1 -0
- package/dist/backend/routes.d.ts +36 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +44 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/gateway-refund-registry.d.ts +122 -0
- package/dist/backend/services/gateway-refund-registry.d.ts.map +1 -0
- package/dist/backend/services/gateway-refund-registry.js +106 -0
- package/dist/backend/services/gateway-refund-registry.js.map +1 -0
- package/dist/backend/services/payment-email-notifier.d.ts +71 -0
- package/dist/backend/services/payment-email-notifier.d.ts.map +1 -0
- package/dist/backend/services/payment-email-notifier.js +73 -0
- package/dist/backend/services/payment-email-notifier.js.map +1 -0
- package/dist/backend/services/payment-email-renderer.d.ts +22 -0
- package/dist/backend/services/payment-email-renderer.d.ts.map +1 -0
- package/dist/backend/services/payment-email-renderer.js +17 -0
- package/dist/backend/services/payment-email-renderer.js.map +1 -0
- package/dist/backend/services/payment-placement-apply-port.d.ts +33 -0
- package/dist/backend/services/payment-placement-apply-port.d.ts.map +1 -0
- package/dist/backend/services/payment-placement-apply-port.js +68 -0
- package/dist/backend/services/payment-placement-apply-port.js.map +1 -0
- package/dist/backend/services/payment-read-port.d.ts +33 -0
- package/dist/backend/services/payment-read-port.d.ts.map +1 -0
- package/dist/backend/services/payment-read-port.js +64 -0
- package/dist/backend/services/payment-read-port.js.map +1 -0
- package/dist/backend/services/payment-reference-port.d.ts +29 -0
- package/dist/backend/services/payment-reference-port.d.ts.map +1 -0
- package/dist/backend/services/payment-reference-port.js +70 -0
- package/dist/backend/services/payment-reference-port.js.map +1 -0
- package/dist/backend/services/payment-refund.d.ts +55 -0
- package/dist/backend/services/payment-refund.d.ts.map +1 -0
- package/dist/backend/services/payment-refund.js +101 -0
- package/dist/backend/services/payment-refund.js.map +1 -0
- package/dist/backend/services/payment-retry-service.d.ts +45 -0
- package/dist/backend/services/payment-retry-service.d.ts.map +1 -0
- package/dist/backend/services/payment-retry-service.js +187 -0
- package/dist/backend/services/payment-retry-service.js.map +1 -0
- package/dist/backend/services/payment-service.d.ts +62 -0
- package/dist/backend/services/payment-service.d.ts.map +1 -0
- package/dist/backend/services/payment-service.js +134 -0
- package/dist/backend/services/payment-service.js.map +1 -0
- package/dist/backend/services/receive-payment-handler.d.ts +260 -0
- package/dist/backend/services/receive-payment-handler.d.ts.map +1 -0
- package/dist/backend/services/receive-payment-handler.js +356 -0
- package/dist/backend/services/receive-payment-handler.js.map +1 -0
- package/dist/backend/services/registry-singleton.d.ts +16 -0
- package/dist/backend/services/registry-singleton.d.ts.map +1 -0
- package/dist/backend/services/registry-singleton.js +17 -0
- package/dist/backend/services/registry-singleton.js.map +1 -0
- package/dist/manifest.d.ts +210 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +182 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260925T115728_payments_refunded_amount.d.ts +28 -0
- package/dist/migrations/20260925T115728_payments_refunded_amount.d.ts.map +1 -0
- package/dist/migrations/20260925T115728_payments_refunded_amount.js +32 -0
- package/dist/migrations/20260925T115728_payments_refunded_amount.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/payments.md +69 -0
- package/i18n/en.json +5 -0
- package/i18n/pl.json +5 -0
- package/package.json +96 -0
- 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"}
|