@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,106 @@
|
|
|
1
|
+
const consoleLogger = {
|
|
2
|
+
warn: (message) => console.warn(message),
|
|
3
|
+
};
|
|
4
|
+
export class GatewayRefundRegistry {
|
|
5
|
+
log;
|
|
6
|
+
isModulePresent;
|
|
7
|
+
handlers = new Map();
|
|
8
|
+
/**
|
|
9
|
+
* @param log collision warnings.
|
|
10
|
+
* @param isModulePresent the effective-state probe. Defaults to
|
|
11
|
+
* always-present, so a registry a unit test builds for itself keeps
|
|
12
|
+
* answering about the handlers that test registered; the process singleton
|
|
13
|
+
* wires it to the kernel's effective state.
|
|
14
|
+
*/
|
|
15
|
+
constructor(log = consoleLogger, isModulePresent = () => true) {
|
|
16
|
+
this.log = log;
|
|
17
|
+
this.isModulePresent = isModulePresent;
|
|
18
|
+
}
|
|
19
|
+
register(handler, moduleId) {
|
|
20
|
+
const existing = this.handlers.get(handler.adapterKey);
|
|
21
|
+
if (existing && existing.module !== moduleId) {
|
|
22
|
+
this.log.warn(`GatewayRefundRegistry: handler "${handler.adapterKey}" re-registered by module ` +
|
|
23
|
+
`"${moduleId}" (was "${existing.module}"); overwriting previous registration.`);
|
|
24
|
+
}
|
|
25
|
+
this.handlers.set(handler.adapterKey, { handler, module: moduleId });
|
|
26
|
+
}
|
|
27
|
+
unregister(adapterKey) {
|
|
28
|
+
this.handlers.delete(adapterKey);
|
|
29
|
+
}
|
|
30
|
+
/** The contributed entry, presence-blind. Diagnostics read this. */
|
|
31
|
+
entry(adapterKey) {
|
|
32
|
+
return this.handlers.get(adapterKey);
|
|
33
|
+
}
|
|
34
|
+
/** The module that contributed `adapterKey`, or `null` when nobody did. */
|
|
35
|
+
ownerOf(adapterKey) {
|
|
36
|
+
return this.handlers.get(adapterKey)?.module ?? null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The module that would have handled this refund but is not present (D-71).
|
|
40
|
+
*
|
|
41
|
+
* The question a caller asks *after* {@link resolve} declined, because one
|
|
42
|
+
* empty `resolve` covers two situations that are not interchangeable: a
|
|
43
|
+
* gateway an operator switched off is a capability restorable in one click
|
|
44
|
+
* and must be **refused**, while an adapter nobody ever registered is a
|
|
45
|
+
* deployment with no PSP integration, which records the obligation and
|
|
46
|
+
* settles by hand.
|
|
47
|
+
*
|
|
48
|
+
* It mirrors `resolve`'s two arms exactly, which is why the unkeyed one is
|
|
49
|
+
* here at all: `resolve` answers with the sole registered handler when the
|
|
50
|
+
* order names no adapter, and point 3 of the docblock above says that is the
|
|
51
|
+
* worst arm to get wrong — the refunds with the least information behind
|
|
52
|
+
* them. Presence-blind in the same sense `ownerOf` is.
|
|
53
|
+
*/
|
|
54
|
+
absentOwnerFor(adapterKey) {
|
|
55
|
+
if (adapterKey) {
|
|
56
|
+
const entry = this.handlers.get(adapterKey);
|
|
57
|
+
if (!entry || this.isModulePresent(entry.module))
|
|
58
|
+
return null;
|
|
59
|
+
return entry.module;
|
|
60
|
+
}
|
|
61
|
+
// No key: `resolve` would have answered with the sole registered handler.
|
|
62
|
+
// Nothing is available and exactly one thing is registered ⇒ that one is
|
|
63
|
+
// the handler the operator switched off.
|
|
64
|
+
if (this.list().length > 0)
|
|
65
|
+
return null;
|
|
66
|
+
const entries = [...this.handlers.values()];
|
|
67
|
+
return entries.length === 1 ? entries[0].module : null;
|
|
68
|
+
}
|
|
69
|
+
/** The handler, or `undefined` when unregistered or its owner is absent. */
|
|
70
|
+
get(adapterKey) {
|
|
71
|
+
const entry = this.handlers.get(adapterKey);
|
|
72
|
+
if (!entry || !this.isModulePresent(entry.module))
|
|
73
|
+
return undefined;
|
|
74
|
+
return entry.handler;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the handler for a gateway payment.
|
|
78
|
+
* - Known `adapterKey` → exact handler only (never guess another PSP).
|
|
79
|
+
* - Missing key → sole registered handler when exactly one exists.
|
|
80
|
+
*
|
|
81
|
+
* Both arms drop a handler whose owner is absent, and the second arm counts
|
|
82
|
+
* only the handlers that are available: with one gateway installed and
|
|
83
|
+
* switched off, "the sole registered handler" must not be it.
|
|
84
|
+
*/
|
|
85
|
+
resolve(adapterKey) {
|
|
86
|
+
if (adapterKey) {
|
|
87
|
+
return this.get(adapterKey);
|
|
88
|
+
}
|
|
89
|
+
const available = this.list();
|
|
90
|
+
const [only, ...rest] = available;
|
|
91
|
+
if (only !== undefined && rest.length === 0)
|
|
92
|
+
return this.get(only);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
/** Adapter keys whose owner is present (stable insertion order). */
|
|
96
|
+
list() {
|
|
97
|
+
return [...this.handlers.entries()]
|
|
98
|
+
.filter(([, entry]) => this.isModulePresent(entry.module))
|
|
99
|
+
.map(([key]) => key);
|
|
100
|
+
}
|
|
101
|
+
/** Every registered adapter key, presence-blind. */
|
|
102
|
+
listAll() {
|
|
103
|
+
return [...this.handlers.keys()];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=gateway-refund-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway-refund-registry.js","sourceRoot":"","sources":["../../../src/backend/services/gateway-refund-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { CustomerAccountReadPort, OrderReadPort, TransactionalEmailSender } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* Why the payment-status e-mail did — or did not — go out (issue #78).
|
|
5
|
+
*
|
|
6
|
+
* `notify` answered `void`, and `void` covered seven situations: no sender
|
|
7
|
+
* wired, no order behind the id, no address on the customer, the three outcomes
|
|
8
|
+
* `send` reports since issue #67, and anything the bare `catch` absorbed. A
|
|
9
|
+
* payment was recorded and nothing anywhere said whether the customer had been
|
|
10
|
+
* told.
|
|
11
|
+
*/
|
|
12
|
+
export type PaymentEmailNotSentReason =
|
|
13
|
+
/** No transactional sender is wired in this composition. */
|
|
14
|
+
'no_sender'
|
|
15
|
+
/** The event named an order this process cannot load. */
|
|
16
|
+
| 'order_not_found'
|
|
17
|
+
/** The customer account carries no address to send to. */
|
|
18
|
+
| 'no_recipient'
|
|
19
|
+
/** The operator switched the `payment_status_changed` e-mail off. */
|
|
20
|
+
| 'deactivated'
|
|
21
|
+
/** No mailer is wired behind the sender. */
|
|
22
|
+
| 'no_transport'
|
|
23
|
+
/** No `payment_status_changed` template exists yet. */
|
|
24
|
+
| 'no_definition'
|
|
25
|
+
/** The send raised, and the payment stays recorded. */
|
|
26
|
+
| 'failed';
|
|
27
|
+
export type PaymentEmailResult = {
|
|
28
|
+
sent: true;
|
|
29
|
+
} | {
|
|
30
|
+
sent: false;
|
|
31
|
+
reason: PaymentEmailNotSentReason;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Where a send that did not happen is reported. Injectable so a test can read
|
|
35
|
+
* it; defaults to `console.warn`, which is what the rest of this layer uses.
|
|
36
|
+
*/
|
|
37
|
+
export type PaymentEmailLog = (message: string, context: Record<string, unknown>) => void;
|
|
38
|
+
export interface PaymentEmailNotifierDeps {
|
|
39
|
+
emFactory: () => EntityManager;
|
|
40
|
+
/**
|
|
41
|
+
* Feature 075 Phase C — the order and its buyer come from the ports their
|
|
42
|
+
* owners publish, not from `Order` and `CustomerAccount`. Both fail closed
|
|
43
|
+
* when their owner is off, and that is the right answer for a notification:
|
|
44
|
+
* an e-mail addressed from data the platform will not read is worse than no
|
|
45
|
+
* e-mail. `notify`'s `catch` re-throws `ModuleDisabledError` first, so the
|
|
46
|
+
* refusal reaches the subscriber rather than being logged as `failed`.
|
|
47
|
+
*/
|
|
48
|
+
orderRead: OrderReadPort;
|
|
49
|
+
customerAccountRead: CustomerAccountReadPort;
|
|
50
|
+
getTransactionalEmailSender: () => TransactionalEmailSender | undefined;
|
|
51
|
+
log?: PaymentEmailLog;
|
|
52
|
+
}
|
|
53
|
+
export declare class PaymentEmailNotifier {
|
|
54
|
+
private readonly deps;
|
|
55
|
+
private readonly log;
|
|
56
|
+
constructor(deps: PaymentEmailNotifierDeps);
|
|
57
|
+
/**
|
|
58
|
+
* Public since feature 072 (T126), and `attach(eventBus)` is gone with it.
|
|
59
|
+
* That method subscribed to the raw bus, which is how a payment-status
|
|
60
|
+
* e-mail went out while this module was switched off; the module now
|
|
61
|
+
* subscribes through `ctx.subscribe`, which stops with it.
|
|
62
|
+
*
|
|
63
|
+
* **Best-effort, and now audible.** The subscriber that calls this has no
|
|
64
|
+
* result to inspect, so every non-sent path is written to the log as well as
|
|
65
|
+
* named in the return value; a payment must not be un-recorded because the
|
|
66
|
+
* notification failed.
|
|
67
|
+
*/
|
|
68
|
+
notify(orderId: string, status: 'paid' | 'failed', failureReason: string | null): Promise<PaymentEmailResult>;
|
|
69
|
+
private notSent;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=payment-email-notifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-email-notifier.d.ts","sourceRoot":"","sources":["../../../src/backend/services/payment-email-notifier.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,uBAAuB,EACvB,aAAa,EACb,wBAAwB,EACzB,MAAM,4BAA4B,CAAC;AAIpC;;;;;;;;GAQG;AACH,MAAM,MAAM,yBAAyB;AACnC,4DAA4D;AAC1D,WAAW;AACb,yDAAyD;GACvD,iBAAiB;AACnB,0DAA0D;GACxD,cAAc;AAChB,qEAAqE;GACnE,aAAa;AACf,4CAA4C;GAC1C,cAAc;AAChB,uDAAuD;GACrD,eAAe;AACjB,uDAAuD;GACrD,QAAQ,CAAC;AAEb,MAAM,MAAM,kBAAkB,GAAG;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,yBAAyB,CAAA;CAAE,CAAC;AAErG;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;AAE1F,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B;;;;;;;OAOG;IACH,SAAS,EAAE,aAAa,CAAC;IACzB,mBAAmB,EAAE,uBAAuB,CAAC;IAC7C,2BAA2B,EAAE,MAAM,wBAAwB,GAAG,SAAS,CAAC;IACxE,GAAG,CAAC,EAAE,eAAe,CAAC;CACvB;AAED,qBAAa,oBAAoB;IAGnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;gBAET,IAAI,EAAE,wBAAwB;IAI3D;;;;;;;;;;OAUG;IACG,MAAM,CACV,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GAAG,QAAQ,EACzB,aAAa,EAAE,MAAM,GAAG,IAAI,GAC3B,OAAO,CAAC,kBAAkB,CAAC;IAqC9B,OAAO,CAAC,OAAO;CAYhB"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Feature 047 — payment_status_changed transactional email. A net-new email
|
|
2
|
+
// (no legacy builder): subscribes to payment.received/failed and sends the
|
|
3
|
+
// admin-editable template to the order's customer. Best-effort; event-bus
|
|
4
|
+
// dispatch isolates handler errors.
|
|
5
|
+
import { rethrowIfModuleDisabled } from '@endora-commerce/platform/kernel';
|
|
6
|
+
import { SalesChannel } from '@endora-commerce/platform/kernel';
|
|
7
|
+
export class PaymentEmailNotifier {
|
|
8
|
+
deps;
|
|
9
|
+
log;
|
|
10
|
+
constructor(deps) {
|
|
11
|
+
this.deps = deps;
|
|
12
|
+
this.log = deps.log ?? ((message, context) => console.warn(message, context));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Public since feature 072 (T126), and `attach(eventBus)` is gone with it.
|
|
16
|
+
* That method subscribed to the raw bus, which is how a payment-status
|
|
17
|
+
* e-mail went out while this module was switched off; the module now
|
|
18
|
+
* subscribes through `ctx.subscribe`, which stops with it.
|
|
19
|
+
*
|
|
20
|
+
* **Best-effort, and now audible.** The subscriber that calls this has no
|
|
21
|
+
* result to inspect, so every non-sent path is written to the log as well as
|
|
22
|
+
* named in the return value; a payment must not be un-recorded because the
|
|
23
|
+
* notification failed.
|
|
24
|
+
*/
|
|
25
|
+
async notify(orderId, status, failureReason) {
|
|
26
|
+
const sender = this.deps.getTransactionalEmailSender();
|
|
27
|
+
if (!sender)
|
|
28
|
+
return this.notSent(orderId, 'no_sender');
|
|
29
|
+
try {
|
|
30
|
+
const em = this.deps.emFactory();
|
|
31
|
+
const order = await this.deps.orderRead.findById(orderId);
|
|
32
|
+
if (!order)
|
|
33
|
+
return this.notSent(orderId, 'order_not_found');
|
|
34
|
+
const customer = await this.deps.customerAccountRead.findById(order.placedByCustomerAccountId);
|
|
35
|
+
if (!customer?.email)
|
|
36
|
+
return this.notSent(orderId, 'no_recipient');
|
|
37
|
+
const channel = await em.findOne(SalesChannel, { id: order.salesChannelId });
|
|
38
|
+
const outcome = await sender.send({
|
|
39
|
+
code: 'payment_status_changed',
|
|
40
|
+
salesChannelId: order.salesChannelId,
|
|
41
|
+
language: channel?.defaultLanguage ?? 'en-US',
|
|
42
|
+
to: customer.email,
|
|
43
|
+
messageId: `payment_status_changed:${order.id}:${status}`,
|
|
44
|
+
variables: {
|
|
45
|
+
order: { businessId: order.businessId },
|
|
46
|
+
payment: { status, statusLabel: status === 'paid' ? 'Paid' : 'Failed', failureReason: failureReason ?? '' },
|
|
47
|
+
},
|
|
48
|
+
meta: { orderId: order.id, kind: 'payment_status_changed', status },
|
|
49
|
+
});
|
|
50
|
+
if (outcome.status !== 'sent')
|
|
51
|
+
return this.notSent(orderId, outcome.status);
|
|
52
|
+
return { sent: true };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// A switched-off module is a presence answer about the whole operation,
|
|
56
|
+
// not a message that failed to render; absorbing it would report "sent
|
|
57
|
+
// nothing" where the truthful answer is "this capability is off".
|
|
58
|
+
rethrowIfModuleDisabled(error);
|
|
59
|
+
// Everything else is contained: the payment is recorded and must not be
|
|
60
|
+
// undone because the message did not go out. It is named, though.
|
|
61
|
+
return this.notSent(orderId, 'failed', error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
notSent(orderId, reason, error) {
|
|
65
|
+
this.log('[payments] the payment-status e-mail was not sent', {
|
|
66
|
+
orderId,
|
|
67
|
+
reason,
|
|
68
|
+
...(error === undefined ? {} : { error: error instanceof Error ? error.message : error }),
|
|
69
|
+
});
|
|
70
|
+
return { sent: false, reason };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=payment-email-notifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-email-notifier.js","sourceRoot":"","sources":["../../../src/backend/services/payment-email-notifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment-method renderer registry for the transactional order-confirmation
|
|
3
|
+
* e-mail (feature 034, FR-016/FR-017). An adapter may register a custom
|
|
4
|
+
* e-mail renderer under a key (the adapter's `renderers.email`); when none is
|
|
5
|
+
* registered, the platform default text builder is used, so the payment
|
|
6
|
+
* section always renders.
|
|
7
|
+
*
|
|
8
|
+
* E-mail bodies are plain text (see Mailer.MailerSendInput.text), so a renderer
|
|
9
|
+
* is a `(ctx) => string` builder rather than a React component.
|
|
10
|
+
*/
|
|
11
|
+
import type { PaymentEmailContext } from '@endora-commerce/contracts';
|
|
12
|
+
/**
|
|
13
|
+
* `PaymentEmailContext` moved to `@endora-commerce/contracts` in feature 075's Phase P —
|
|
14
|
+
* `orders` renders the payment line with it. Re-exported here for the length
|
|
15
|
+
* of Phase P, which cuts no consumer.
|
|
16
|
+
*/
|
|
17
|
+
export type { PaymentEmailContext };
|
|
18
|
+
export type PaymentEmailRenderer = (ctx: PaymentEmailContext) => string;
|
|
19
|
+
export declare const defaultPaymentEmailRenderer: PaymentEmailRenderer;
|
|
20
|
+
export declare function registerPaymentEmailRenderer(key: string, renderer: PaymentEmailRenderer): void;
|
|
21
|
+
export declare function resolvePaymentEmailRenderer(rendererKey: string | null): PaymentEmailRenderer;
|
|
22
|
+
//# sourceMappingURL=payment-email-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-email-renderer.d.ts","sourceRoot":"","sources":["../../../src/backend/services/payment-email-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE;;;;GAIG;AACH,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,MAAM,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,mBAAmB,KAAK,MAAM,CAAC;AAExE,eAAO,MAAM,2BAA2B,EAAE,oBAIzC,CAAC;AAIF,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAE9F;AAED,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,oBAAoB,CAM5F"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const defaultPaymentEmailRenderer = (ctx) => {
|
|
2
|
+
const surcharge = ctx.additionalPrice > 0 ? ` (+${ctx.additionalPrice.toFixed(2)} ${ctx.currency})` : '';
|
|
3
|
+
return `${ctx.name}${surcharge}`;
|
|
4
|
+
};
|
|
5
|
+
const registry = new Map();
|
|
6
|
+
export function registerPaymentEmailRenderer(key, renderer) {
|
|
7
|
+
registry.set(key, renderer);
|
|
8
|
+
}
|
|
9
|
+
export function resolvePaymentEmailRenderer(rendererKey) {
|
|
10
|
+
if (rendererKey) {
|
|
11
|
+
const found = registry.get(rendererKey);
|
|
12
|
+
if (found)
|
|
13
|
+
return found;
|
|
14
|
+
}
|
|
15
|
+
return defaultPaymentEmailRenderer;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=payment-email-renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-email-renderer.js","sourceRoot":"","sources":["../../../src/backend/services/payment-email-renderer.ts"],"names":[],"mappings":"AAqBA,MAAM,CAAC,MAAM,2BAA2B,GAAyB,CAAC,GAAG,EAAE,EAAE;IACvE,MAAM,SAAS,GACb,GAAG,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACzF,OAAO,GAAG,GAAG,CAAC,IAAI,GAAG,SAAS,EAAE,CAAC;AACnC,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAgC,CAAC;AAEzD,MAAM,UAAU,4BAA4B,CAAC,GAAW,EAAE,QAA8B;IACtF,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,WAA0B;IACpE,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACxC,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,OAAO,2BAA2B,CAAC;AACrC,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { PaymentOpened, PaymentPlacementApplyPort } from '@endora-commerce/mod-orders/ports';
|
|
3
|
+
/**
|
|
4
|
+
* `paymentPlacementApplyPort` — the payment row an order is placed with,
|
|
5
|
+
* written on the caller's `EntityManager` (feature 080, T048; D-169, D-179).
|
|
6
|
+
*
|
|
7
|
+
* `orders` held this module's `Payment` class for it until T048 and did the
|
|
8
|
+
* `tx.create` itself — the last cross-module entity-class reach in the tree, and
|
|
9
|
+
* the one thing standing between `orders` and `payments` and their packaging.
|
|
10
|
+
* D-168 leaves a packaged `payments` no entity class for `orders` to name, so
|
|
11
|
+
* the reach had to go before either module moves; the transaction it runs in did
|
|
12
|
+
* not change, and `payments_order_fk` is untouched. The port's own doc block
|
|
13
|
+
* states the constraint.
|
|
14
|
+
*
|
|
15
|
+
* **Every default the row carries is this module's**, and that is the second
|
|
16
|
+
* thing the conversion moved. `status` (`awaiting_payment`), `attemptNo` (1) and
|
|
17
|
+
* `refundedAmount` (`0`) are entity defaults `orders` inherited by constructing
|
|
18
|
+
* the row; they are now inherited by the module that owns the vocabulary, which
|
|
19
|
+
* is what makes `deferred` — the one value a placement ever overrides — a named
|
|
20
|
+
* method below rather than a string crossing a boundary.
|
|
21
|
+
*/
|
|
22
|
+
export declare class PaymentPlacementApplyService implements PaymentPlacementApplyPort {
|
|
23
|
+
openForOrder(em: EntityManager, input: {
|
|
24
|
+
orderId: string;
|
|
25
|
+
paymentMethodId: string;
|
|
26
|
+
amount: string;
|
|
27
|
+
currency: string;
|
|
28
|
+
}): Promise<PaymentOpened>;
|
|
29
|
+
markDeferred(em: EntityManager, input: {
|
|
30
|
+
paymentId: string;
|
|
31
|
+
}): Promise<PaymentOpened>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=payment-placement-apply-port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-placement-apply-port.d.ts","sourceRoot":"","sources":["../../../src/backend/services/payment-placement-apply-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAI3D,OAAO,KAAK,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAElG;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,4BAA6B,YAAW,yBAAyB;IACtE,YAAY,CAChB,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE;QACL,OAAO,EAAE,MAAM,CAAC;QAChB,eAAe,EAAE,MAAM,CAAC;QACxB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,GACA,OAAO,CAAC,aAAa,CAAC;IAenB,YAAY,CAChB,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAC3B,OAAO,CAAC,aAAa,CAAC;CAsB1B"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { Payment } from '../entities/payment.entity.js';
|
|
4
|
+
/**
|
|
5
|
+
* `paymentPlacementApplyPort` — the payment row an order is placed with,
|
|
6
|
+
* written on the caller's `EntityManager` (feature 080, T048; D-169, D-179).
|
|
7
|
+
*
|
|
8
|
+
* `orders` held this module's `Payment` class for it until T048 and did the
|
|
9
|
+
* `tx.create` itself — the last cross-module entity-class reach in the tree, and
|
|
10
|
+
* the one thing standing between `orders` and `payments` and their packaging.
|
|
11
|
+
* D-168 leaves a packaged `payments` no entity class for `orders` to name, so
|
|
12
|
+
* the reach had to go before either module moves; the transaction it runs in did
|
|
13
|
+
* not change, and `payments_order_fk` is untouched. The port's own doc block
|
|
14
|
+
* states the constraint.
|
|
15
|
+
*
|
|
16
|
+
* **Every default the row carries is this module's**, and that is the second
|
|
17
|
+
* thing the conversion moved. `status` (`awaiting_payment`), `attemptNo` (1) and
|
|
18
|
+
* `refundedAmount` (`0`) are entity defaults `orders` inherited by constructing
|
|
19
|
+
* the row; they are now inherited by the module that owns the vocabulary, which
|
|
20
|
+
* is what makes `deferred` — the one value a placement ever overrides — a named
|
|
21
|
+
* method below rather than a string crossing a boundary.
|
|
22
|
+
*/
|
|
23
|
+
export class PaymentPlacementApplyService {
|
|
24
|
+
async openForOrder(em, input) {
|
|
25
|
+
// command-coverage-ignore: the payment row an audited order placement opens,
|
|
26
|
+
// inside that placement's own transaction. `orders` records the placement as
|
|
27
|
+
// one operation through its own Command; a second audit entry for the row it
|
|
28
|
+
// opened would record the same decision twice.
|
|
29
|
+
const payment = em.create(Payment, {
|
|
30
|
+
orderId: input.orderId,
|
|
31
|
+
paymentMethodId: input.paymentMethodId,
|
|
32
|
+
amount: input.amount,
|
|
33
|
+
currency: input.currency,
|
|
34
|
+
});
|
|
35
|
+
await em.persistAndFlush(payment);
|
|
36
|
+
return recordOf(payment);
|
|
37
|
+
}
|
|
38
|
+
async markDeferred(em, input) {
|
|
39
|
+
const payment = await em.findOne(Payment, { id: input.paymentId });
|
|
40
|
+
if (!payment) {
|
|
41
|
+
// Not a tolerance and not a degrade: the one caller opened this row on the
|
|
42
|
+
// very transaction it is handing back, so an absent row means the two
|
|
43
|
+
// calls are not on one unit of work — which is precisely the mistake the
|
|
44
|
+
// required `EntityManager` exists to make impossible. Refuse loudly rather
|
|
45
|
+
// than leave a credit-limit order recorded as awaiting a payment nobody
|
|
46
|
+
// will make.
|
|
47
|
+
throw new HttpError(500, ERROR_CODES.INTERNAL, 'Payment row to defer does not exist on this transaction.');
|
|
48
|
+
}
|
|
49
|
+
payment.status = 'deferred';
|
|
50
|
+
// command-coverage-ignore: the same placement operation as `openForOrder`
|
|
51
|
+
// above, one branch further in — the credit-limit order whose reservation
|
|
52
|
+
// this transaction has just taken. Audited once, by `orders`' own Command.
|
|
53
|
+
await em.flush();
|
|
54
|
+
return recordOf(payment);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** A published record, never the managed entity (D-77's first narrowing). */
|
|
58
|
+
function recordOf(payment) {
|
|
59
|
+
return {
|
|
60
|
+
id: payment.id,
|
|
61
|
+
orderId: payment.orderId,
|
|
62
|
+
paymentMethodId: payment.paymentMethodId,
|
|
63
|
+
amount: payment.amount,
|
|
64
|
+
currency: payment.currency,
|
|
65
|
+
status: payment.status,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=payment-placement-apply-port.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-placement-apply-port.js","sourceRoot":"","sources":["../../../src/backend/services/payment-placement-apply-port.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAGxD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,4BAA4B;IACvC,KAAK,CAAC,YAAY,CAChB,EAAiB,EACjB,KAKC;QAED,6EAA6E;QAC7E,6EAA6E;QAC7E,6EAA6E;QAC7E,+CAA+C;QAC/C,MAAM,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE;YACjC,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,eAAe,EAAE,KAAK,CAAC,eAAe;YACtC,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;SACzB,CAAC,CAAC;QACH,MAAM,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;QAClC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,YAAY,CAChB,EAAiB,EACjB,KAA4B;QAE5B,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC;QACnE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,2EAA2E;YAC3E,sEAAsE;YACtE,yEAAyE;YACzE,2EAA2E;YAC3E,wEAAwE;YACxE,aAAa;YACb,MAAM,IAAI,SAAS,CACjB,GAAG,EACH,WAAW,CAAC,QAAQ,EACpB,0DAA0D,CAC3D,CAAC;QACJ,CAAC;QACD,OAAO,CAAC,MAAM,GAAG,UAAU,CAAC;QAC5B,0EAA0E;QAC1E,0EAA0E;QAC1E,2EAA2E;QAC3E,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;CACF;AAED,6EAA6E;AAC7E,SAAS,QAAQ,CAAC,OAAgB;IAChC,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,eAAe,EAAE,OAAO,CAAC,eAAe;QACxC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { PaymentReadPort, PaymentRecord } from '@endora-commerce/contracts';
|
|
3
|
+
import { Payment } from '../entities/payment.entity.js';
|
|
4
|
+
/**
|
|
5
|
+
* The row-level read model `payments` publishes (feature 075, Phase P).
|
|
6
|
+
*
|
|
7
|
+
* Sixteen of this module's 35 inbound import sites are `em.findOne(Payment, …)`
|
|
8
|
+
* — the same four lookups written out once per gateway, with each gateway
|
|
9
|
+
* spelling "the latest attempt" slightly differently (`attemptNo` descending
|
|
10
|
+
* in two, `createdAt` descending in the others). Publishing the read fixes
|
|
11
|
+
* that ordering in one place: `attemptNo` first, `createdAt` as the tiebreak,
|
|
12
|
+
* which is the ordering the attempt counter is maintained for.
|
|
13
|
+
*
|
|
14
|
+
* The money columns stay strings: they are `decimal(14,2)`, and every consumer
|
|
15
|
+
* forwards the figure to a payment provider.
|
|
16
|
+
*
|
|
17
|
+
* `countByPaymentMethod` joined it for feature 075. It answers the one question
|
|
18
|
+
* `payment_methods` had been asking of this module's table directly, in raw SQL
|
|
19
|
+
* inside its delete Command — a boundary crossing that named no import
|
|
20
|
+
* specifier and so compiled, returned rows and went unseen until
|
|
21
|
+
* `check:module-boundary` learned to read a SQL statement.
|
|
22
|
+
*/
|
|
23
|
+
export declare class PaymentReadService implements PaymentReadPort {
|
|
24
|
+
private readonly emFactory;
|
|
25
|
+
constructor(emFactory: () => EntityManager);
|
|
26
|
+
findById(id: string): Promise<PaymentRecord | null>;
|
|
27
|
+
listByOrderId(orderId: string): Promise<PaymentRecord[]>;
|
|
28
|
+
findLatestForOrder(orderId: string): Promise<PaymentRecord | null>;
|
|
29
|
+
findByExternalReference(externalReference: string): Promise<PaymentRecord | null>;
|
|
30
|
+
countByPaymentMethod(paymentMethodId: string): Promise<number>;
|
|
31
|
+
}
|
|
32
|
+
export declare function toPaymentRecord(payment: Payment): PaymentRecord;
|
|
33
|
+
//# sourceMappingURL=payment-read-port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-read-port.d.ts","sourceRoot":"","sources":["../../../src/backend/services/payment-read-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AACjF,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAExD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,kBAAmB,YAAW,eAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAKnD,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IASxD,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IASlE,uBAAuB,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IASjF,oBAAoB,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAGrE;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,CAiB/D"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Payment } from '../entities/payment.entity.js';
|
|
2
|
+
/**
|
|
3
|
+
* The row-level read model `payments` publishes (feature 075, Phase P).
|
|
4
|
+
*
|
|
5
|
+
* Sixteen of this module's 35 inbound import sites are `em.findOne(Payment, …)`
|
|
6
|
+
* — the same four lookups written out once per gateway, with each gateway
|
|
7
|
+
* spelling "the latest attempt" slightly differently (`attemptNo` descending
|
|
8
|
+
* in two, `createdAt` descending in the others). Publishing the read fixes
|
|
9
|
+
* that ordering in one place: `attemptNo` first, `createdAt` as the tiebreak,
|
|
10
|
+
* which is the ordering the attempt counter is maintained for.
|
|
11
|
+
*
|
|
12
|
+
* The money columns stay strings: they are `decimal(14,2)`, and every consumer
|
|
13
|
+
* forwards the figure to a payment provider.
|
|
14
|
+
*
|
|
15
|
+
* `countByPaymentMethod` joined it for feature 075. It answers the one question
|
|
16
|
+
* `payment_methods` had been asking of this module's table directly, in raw SQL
|
|
17
|
+
* inside its delete Command — a boundary crossing that named no import
|
|
18
|
+
* specifier and so compiled, returned rows and went unseen until
|
|
19
|
+
* `check:module-boundary` learned to read a SQL statement.
|
|
20
|
+
*/
|
|
21
|
+
export class PaymentReadService {
|
|
22
|
+
emFactory;
|
|
23
|
+
constructor(emFactory) {
|
|
24
|
+
this.emFactory = emFactory;
|
|
25
|
+
}
|
|
26
|
+
async findById(id) {
|
|
27
|
+
const payment = await this.emFactory().findOne(Payment, { id });
|
|
28
|
+
return payment ? toPaymentRecord(payment) : null;
|
|
29
|
+
}
|
|
30
|
+
async listByOrderId(orderId) {
|
|
31
|
+
const payments = await this.emFactory().find(Payment, { orderId }, { orderBy: { attemptNo: 'desc', createdAt: 'desc' } });
|
|
32
|
+
return payments.map(toPaymentRecord);
|
|
33
|
+
}
|
|
34
|
+
async findLatestForOrder(orderId) {
|
|
35
|
+
const payment = await this.emFactory().findOne(Payment, { orderId }, { orderBy: { attemptNo: 'desc', createdAt: 'desc' } });
|
|
36
|
+
return payment ? toPaymentRecord(payment) : null;
|
|
37
|
+
}
|
|
38
|
+
async findByExternalReference(externalReference) {
|
|
39
|
+
const payment = await this.emFactory().findOne(Payment, { externalReference }, { orderBy: { attemptNo: 'desc', createdAt: 'desc' } });
|
|
40
|
+
return payment ? toPaymentRecord(payment) : null;
|
|
41
|
+
}
|
|
42
|
+
async countByPaymentMethod(paymentMethodId) {
|
|
43
|
+
return this.emFactory().count(Payment, { paymentMethodId });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function toPaymentRecord(payment) {
|
|
47
|
+
return {
|
|
48
|
+
id: payment.id,
|
|
49
|
+
orderId: payment.orderId,
|
|
50
|
+
paymentMethodId: payment.paymentMethodId,
|
|
51
|
+
status: payment.status,
|
|
52
|
+
amount: payment.amount,
|
|
53
|
+
refundedAmount: payment.refundedAmount,
|
|
54
|
+
currency: payment.currency,
|
|
55
|
+
paidAt: payment.paidAt ?? null,
|
|
56
|
+
externalReference: payment.externalReference ?? null,
|
|
57
|
+
providerDetails: payment.providerDetails ?? null,
|
|
58
|
+
failureReason: payment.failureReason ?? null,
|
|
59
|
+
attemptNo: payment.attemptNo,
|
|
60
|
+
createdAt: payment.createdAt,
|
|
61
|
+
updatedAt: payment.updatedAt,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=payment-read-port.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-read-port.js","sourceRoot":"","sources":["../../../src/backend/services/payment-read-port.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAExD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,kBAAkB;IACA;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,QAAQ,CAAC,EAAU;QACvB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QAChE,OAAO,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACnD,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,OAAe;QACjC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,CAC1C,OAAO,EACP,EAAE,OAAO,EAAE,EACX,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,CACtD,CAAC;QACF,OAAO,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACvC,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,OAAe;QACtC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAC5C,OAAO,EACP,EAAE,OAAO,EAAE,EACX,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,CACtD,CAAC;QACF,OAAO,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACnD,CAAC;IAED,KAAK,CAAC,uBAAuB,CAAC,iBAAyB;QACrD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAC5C,OAAO,EACP,EAAE,iBAAiB,EAAE,EACrB,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,CACtD,CAAC;QACF,OAAO,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACnD,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,eAAuB;QAChD,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,eAAe,EAAE,CAAC,CAAC;IAC9D,CAAC;CACF;AAED,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,eAAe,EAAE,OAAO,CAAC,eAAe;QACxC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI;QAC9B,iBAAiB,EAAE,OAAO,CAAC,iBAAiB,IAAI,IAAI;QACpD,eAAe,EAAE,OAAO,CAAC,eAAe,IAAI,IAAI;QAChD,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,IAAI;QAC5C,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS,EAAE,OAAO,CAAC,SAAS;KAC7B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { PaymentReferencePort } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* The one write the four payment gateways make into this module's table
|
|
5
|
+
* (feature 075, Phase P) — the write side of
|
|
6
|
+
* `PaymentReadPort.findByExternalReference`.
|
|
7
|
+
*
|
|
8
|
+
* Each gateway opens its own provider object for an attempt (a Stripe
|
|
9
|
+
* PaymentIntent or Checkout Session, a TPay transaction, a PayU order, an
|
|
10
|
+
* Autopay transaction) and then records that object's identifier on the
|
|
11
|
+
* attempt, so a later provider event carrying only the provider's reference
|
|
12
|
+
* resolves back to a payment. Four modules were doing it with
|
|
13
|
+
* `em.findOne(Payment, …)` and a field assignment, which is this module's row
|
|
14
|
+
* being written by somebody else's `EntityManager`.
|
|
15
|
+
*
|
|
16
|
+
* It is deliberately not on the Command Bus, and the four call sites all say
|
|
17
|
+
* why in their own words: this is provider-integration bookkeeping, not an
|
|
18
|
+
* operator decision. The state change an operator is answerable for is the
|
|
19
|
+
* settlement, and that is audited in the payments/orders flow.
|
|
20
|
+
*/
|
|
21
|
+
export declare class PaymentReferenceService implements PaymentReferencePort {
|
|
22
|
+
private readonly emFactory;
|
|
23
|
+
constructor(emFactory: () => EntityManager);
|
|
24
|
+
stampExternalReference(paymentId: string, externalReference: string): Promise<boolean>;
|
|
25
|
+
stampExternalReferenceIfAbsent(paymentId: string, externalReference: string): Promise<boolean>;
|
|
26
|
+
mergeProviderDetails(paymentId: string, patch: Record<string, unknown>): Promise<boolean>;
|
|
27
|
+
private stamp;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=payment-reference-port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-reference-port.d.ts","sourceRoot":"","sources":["../../../src/backend/services/payment-reference-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAGvE;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,uBAAwB,YAAW,oBAAoB;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQtF,8BAA8B,CAClC,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,OAAO,CAAC;IAMb,oBAAoB,CACxB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,OAAO,CAAC;YAYL,KAAK;CAqBpB"}
|