@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,318 @@
|
|
|
1
|
+
import { lazyPort } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import { builtInPaymentAdapters } from './adapters/built-in-adapters.js';
|
|
3
|
+
import { Payment } from './entities/payment.entity.js';
|
|
4
|
+
import { ReceivePaymentHandler, } from './services/receive-payment-handler.js';
|
|
5
|
+
import { PaymentService } from './services/payment-service.js';
|
|
6
|
+
import { PaymentRetryService } from './services/payment-retry-service.js';
|
|
7
|
+
import { PaymentReadService } from './services/payment-read-port.js';
|
|
8
|
+
import { PaymentReferenceService } from './services/payment-reference-port.js';
|
|
9
|
+
import { PaymentPlacementApplyService } from './services/payment-placement-apply-port.js';
|
|
10
|
+
import { PaymentRefundProvider } from './services/payment-refund.js';
|
|
11
|
+
import { gatewayRefundRegistry } from './services/registry-singleton.js';
|
|
12
|
+
import { PaymentEmailNotifier } from './services/payment-email-notifier.js';
|
|
13
|
+
import { resolvePaymentEmailRenderer } from './services/payment-email-renderer.js';
|
|
14
|
+
import { registerPaymentsRoutes } from './routes.js';
|
|
15
|
+
import { registerPaymentsCustomerRoutes } from './routes.customer.js';
|
|
16
|
+
import { PAYMENT_STATUS_CHANGED_DEFAULT } from './email-templates/transactional-defaults.js';
|
|
17
|
+
export function registerModule(ctx) {
|
|
18
|
+
ctx.di.register({
|
|
19
|
+
/**
|
|
20
|
+
* The refund-handler table, as the container's value — the delivery-side
|
|
21
|
+
* twin of `payment_methods`' `paymentAdapterRegistry`, and registered the
|
|
22
|
+
* same way and for the same reason (feature 075, Phase P).
|
|
23
|
+
*
|
|
24
|
+
* **Ungated, and the only name over this registry** (D-99.7). It used to
|
|
25
|
+
* have a gated twin, `gatewayRefundRegistryPort`, for the *pull* direction —
|
|
26
|
+
* "which handler settles this refund" — but that question is asked inside
|
|
27
|
+
* this module, by `PaymentRefundProvider`, and no module ever resolved the
|
|
28
|
+
* port. A *push* must not be gated: the four gateways contribute their
|
|
29
|
+
* handler from a boot hook, and a gate there would refuse the contribution
|
|
30
|
+
* rather than defer it, so a gateway would silently stay unregistered until
|
|
31
|
+
* the next restart after an operator switched `payments` back on. That
|
|
32
|
+
* asymmetry is now stated in the contract rather than only here, because a
|
|
33
|
+
* rule in a comment ships in no package. The absent-owner policy sits inside
|
|
34
|
+
* the registry, where the whole design puts it: a handler whose module is
|
|
35
|
+
* off is skipped at enumeration and the obligation lands on
|
|
36
|
+
* `pending_manual` (D-71), which a gate over the push would drop instead of
|
|
37
|
+
* record.
|
|
38
|
+
*/
|
|
39
|
+
gatewayRefundRegistry: ctx.asFunction(() => gatewayRefundRegistry).singleton(),
|
|
40
|
+
// Contribution point, defaulted to no sender: a deployment without a
|
|
41
|
+
// transactional-email surface sends nothing rather than failing to settle.
|
|
42
|
+
paymentEmailSender: ctx
|
|
43
|
+
.asFunction(() => () => undefined)
|
|
44
|
+
.singleton(),
|
|
45
|
+
paymentEmailNotifier: ctx
|
|
46
|
+
.asFunction(({ emFactory }) => new PaymentEmailNotifier({
|
|
47
|
+
emFactory,
|
|
48
|
+
orderRead: lazyPort(ctx, 'orderReadPort'),
|
|
49
|
+
customerAccountRead: lazyPort(ctx, 'customerAccountReadPort'),
|
|
50
|
+
// Read per call: a root contributes the sender after
|
|
51
|
+
// `transactional_emails` announces it, which is later than this.
|
|
52
|
+
getTransactionalEmailSender: () => ctx.cradle().paymentEmailSender(),
|
|
53
|
+
}))
|
|
54
|
+
.singleton(),
|
|
55
|
+
});
|
|
56
|
+
ctx.di.providePort('paymentService', ctx
|
|
57
|
+
.asFunction(({ emFactory }) => new PaymentService(emFactory, lazyPort(ctx, 'orderReadPort')))
|
|
58
|
+
.singleton());
|
|
59
|
+
/**
|
|
60
|
+
* The buyer's own retry (issue #264), as this module's own service rather
|
|
61
|
+
* than a route body.
|
|
62
|
+
*
|
|
63
|
+
* It is not a published port: nothing outside this module calls it, and the
|
|
64
|
+
* one surface it has is the HTTP route below. The adapter registry is read
|
|
65
|
+
* per call — an operator can switch a gateway off between two requests, and
|
|
66
|
+
* the registry filters its enumeration on the contributor's effective state.
|
|
67
|
+
*/
|
|
68
|
+
ctx.di.register({
|
|
69
|
+
paymentRetryService: ctx
|
|
70
|
+
.asFunction(() => new PaymentRetryService({
|
|
71
|
+
orderRead: lazyPort(ctx, 'orderReadPort'),
|
|
72
|
+
customerAccountRead: lazyPort(ctx, 'customerAccountReadPort'),
|
|
73
|
+
// Lazily, even though this module owns it: capturing a gate into a
|
|
74
|
+
// singleton is a gate that keeps answering after an operator
|
|
75
|
+
// switches the owner off.
|
|
76
|
+
paymentService: lazyPort(ctx, 'paymentService'),
|
|
77
|
+
// The terminality read (feature 085 Phase D): the same port the
|
|
78
|
+
// settlement handler writes through, resolved for its read half.
|
|
79
|
+
orderTransition: lazyPort(ctx, 'orderTransitionPort'),
|
|
80
|
+
paymentAdapterRegistry: () => ctx.cradle().paymentAdapterRegistry,
|
|
81
|
+
// No `catch` around the port call, for the reason `orders` and
|
|
82
|
+
// `carts` give at the same seam: a disabled-module throw read as
|
|
83
|
+
// "no restriction" would be fail-open on a path whose job is to
|
|
84
|
+
// restrict.
|
|
85
|
+
assertOrganizationCanTransact: async (organizationId) => {
|
|
86
|
+
await ctx.cradle().organizationReadPort.assertCanTransact(organizationId);
|
|
87
|
+
},
|
|
88
|
+
}))
|
|
89
|
+
.singleton(),
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* How a return settlement refunds a payment (feature 046 R5), as this
|
|
93
|
+
* module's port instead of a class both roots constructed (T143c).
|
|
94
|
+
*
|
|
95
|
+
* `PaymentRefundProvider` resolves the order's PSP adapter out of
|
|
96
|
+
* `gatewayRefundRegistry` — this module's registry — so a root's instance
|
|
97
|
+
* refunded through a switched-off `payments`. The bridge `returns` receives
|
|
98
|
+
* forwards to this name per settlement, which is where the gate belongs.
|
|
99
|
+
*/
|
|
100
|
+
ctx.di.providePort('paymentRefundPort', ctx
|
|
101
|
+
.asFunction(() => new PaymentRefundProvider(lazyPort(ctx, 'orderReadPort'), lazyPort(ctx, 'paymentMethodReadPort')))
|
|
102
|
+
.singleton());
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Feature 075, Phase P — the published surface.
|
|
105
|
+
//
|
|
106
|
+
// `paymentReadPort` replaces sixteen hand-written `em.findOne(Payment, …)`
|
|
107
|
+
// calls in the four gateways, and `receivePaymentPort` is the ingress they
|
|
108
|
+
// already share, now expressed without the `Payment` entity in its result
|
|
109
|
+
// type.
|
|
110
|
+
//
|
|
111
|
+
// **The refund registry is published under its push name only** (D-99.7). It
|
|
112
|
+
// once had a second, gated registration here — `gatewayRefundRegistryPort`,
|
|
113
|
+
// over the same instance — which nothing ever resolved, while the doc block in
|
|
114
|
+
// `@endora-commerce/contracts` described the *push* seam above it. An out-of-tree gateway
|
|
115
|
+
// reading only the published contracts would have resolved the gate from its
|
|
116
|
+
// boot hook and exited 1. The contract now names `gatewayRefundRegistry`, the
|
|
117
|
+
// ungated registration above, and says why it is ungated; the pull is
|
|
118
|
+
// intra-module and unpublished.
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
ctx.di.providePort('paymentReadPort', ctx.asFunction(({ emFactory }) => new PaymentReadService(emFactory)).singleton());
|
|
121
|
+
/**
|
|
122
|
+
* The payment row order placement opens, on **placement's** `EntityManager`
|
|
123
|
+
* (feature 080, T048; D-169, D-179).
|
|
124
|
+
*
|
|
125
|
+
* `orders` wrote it with this module's `Payment` class until T048 — the last
|
|
126
|
+
* cross-module entity-class reach in the tree. D-168 leaves a packaged
|
|
127
|
+
* `payments` no entity class for a stranger to name, so what crosses is now
|
|
128
|
+
* this module's own interface and the statement belongs to the module that
|
|
129
|
+
* owns the table. `PaymentPlacementApplyPort` stays out of
|
|
130
|
+
* `@endora-commerce/contracts` because it takes a MikroORM `EntityManager` and
|
|
131
|
+
* FR-034 keeps that package free of them — and it is declared on
|
|
132
|
+
* **`@endora-commerce/mod-orders/ports`**, not this module's own, which is
|
|
133
|
+
* D-171.1 and not an accident. The two modules reach each other, so only one
|
|
134
|
+
* npm edge can exist (a mutual devDependency between two module packages is a
|
|
135
|
+
* build deadlock, `noEmitOnError` making the loser emit nothing), and it runs
|
|
136
|
+
* in the direction of the binding manifest edge: this module declares `orders`
|
|
137
|
+
* in its `dependencies`, so `mod-payments` build-depending on `mod-orders`
|
|
138
|
+
* adds no claim its manifest does not already make.
|
|
139
|
+
*
|
|
140
|
+
* The licence is conditional and the condition is **this** module's:
|
|
141
|
+
* `PaymentPlacementApplyService` names the interface at its `implements`
|
|
142
|
+
* clause, and the registration below carries the explicit type argument. Those
|
|
143
|
+
* are the two places `tsc` checks conformance — TS2420 and TS2345 — and both
|
|
144
|
+
* resolve the interface wherever it was declared. Dropping either turns the
|
|
145
|
+
* seam into D-77's rejected alternative, which is what
|
|
146
|
+
* `check:port-shape`'s `declared-elsewhere-without-implements` refuses.
|
|
147
|
+
*
|
|
148
|
+
* It is a **gated** registration like every other name here, and that gate is
|
|
149
|
+
* the half `orders` writing the row itself could never have. The reachable
|
|
150
|
+
* consequence is narrow, because `assertPaymentMethodUsable` already refuses a
|
|
151
|
+
* method whose adapter this module contributes while this module is absent —
|
|
152
|
+
* D-179 measured that and found checkout already fail-closed, where no money
|
|
153
|
+
* is at stake. What that guard deliberately tolerates is a method whose
|
|
154
|
+
* adapter **no** module ever registered, and such a placement went on writing
|
|
155
|
+
* a row into this module's own table with an operator having switched it off:
|
|
156
|
+
* issue #188's shape, which the gate closes. `orders` declares the edge
|
|
157
|
+
* `refuses-without` and wraps the call in no `catch`.
|
|
158
|
+
*/
|
|
159
|
+
ctx.di.providePort('paymentPlacementApplyPort', ctx.asFunction(() => new PaymentPlacementApplyService()).singleton());
|
|
160
|
+
/**
|
|
161
|
+
* The write side of `findByExternalReference`, published for the four
|
|
162
|
+
* gateways that were doing it with this module's entity and their own
|
|
163
|
+
* `EntityManager`.
|
|
164
|
+
*
|
|
165
|
+
* A port, not a registration: unlike the registry above, this is a pull with
|
|
166
|
+
* a failure mode. A gateway that has just opened a PaymentIntent and cannot
|
|
167
|
+
* record its identifier has to hear so — the provider object is live, and an
|
|
168
|
+
* event it sends would arrive with nothing on this side able to resolve it.
|
|
169
|
+
*/
|
|
170
|
+
ctx.di.providePort('paymentReferencePort', ctx
|
|
171
|
+
.asFunction(({ emFactory }) => new PaymentReferenceService(emFactory))
|
|
172
|
+
.singleton());
|
|
173
|
+
ctx.di.providePort('paymentEmailRendererPort', ctx
|
|
174
|
+
.asFunction(() => ({
|
|
175
|
+
render: (rendererKey, emailContext) => resolvePaymentEmailRenderer(rendererKey)(emailContext),
|
|
176
|
+
}))
|
|
177
|
+
.singleton());
|
|
178
|
+
ctx.di.providePort('receivePaymentPort', ctx
|
|
179
|
+
.asFunction(() => {
|
|
180
|
+
const handler = () => ctx.cradle().receivePaymentHandler;
|
|
181
|
+
return {
|
|
182
|
+
receive: (input) => handler().receive(input),
|
|
183
|
+
reflectRefund: (input) => handler().reflectRefund(input),
|
|
184
|
+
};
|
|
185
|
+
})
|
|
186
|
+
.singleton());
|
|
187
|
+
/**
|
|
188
|
+
* The settlement handler, with the two cross-module reads it used to make
|
|
189
|
+
* with somebody else's entity now made through their ports (feature 075,
|
|
190
|
+
* C-W3).
|
|
191
|
+
*
|
|
192
|
+
* Both edges were ledgered as blocked on this constructor: the four gateways
|
|
193
|
+
* built their own handler, so it could take no port they could not build.
|
|
194
|
+
* They resolve `receivePaymentPort` since the C-W3 gateway cuts, so this is
|
|
195
|
+
* the only construction left and `ctx` is in hand.
|
|
196
|
+
*
|
|
197
|
+
* **The `Order` entity import inside the handler is gone since feature 080's
|
|
198
|
+
* T048**, and the seam it stood for is not: `payments_order_fk` still holds
|
|
199
|
+
* the payment row and the order's payment status co-transactional (D-78 point
|
|
200
|
+
* 2), so the write still runs on the settlement's own `EntityManager` — it is
|
|
201
|
+
* `orderPaymentStatusApplyPort` that performs it now, an interface `orders`
|
|
202
|
+
* declares in its `ports/` directory precisely because an `EntityManager`
|
|
203
|
+
* parameter bars it from `@endora-commerce/contracts` (FR-034). D-168 leaves a
|
|
204
|
+
* packaged `orders` no entity class for this module to name, which is what
|
|
205
|
+
* made the conversion due before that module moves.
|
|
206
|
+
*
|
|
207
|
+
* The three `orders`/`payment_methods` ports this constructor takes are not
|
|
208
|
+
* three of a kind, and the difference is which transaction each runs in:
|
|
209
|
+
* `paymentMethodReadPort` is a read of a row the settlement never writes,
|
|
210
|
+
* `orderPaymentStatusApplyPort` is the co-transactional write above, and
|
|
211
|
+
* `orderTransitionPort` is called after the commit.
|
|
212
|
+
*
|
|
213
|
+
* **`orderTransitionPort` replaces two of the three names this used to take**
|
|
214
|
+
* (feature 085 Phase D). `orderStatusAnnouncePort` goes because the transition
|
|
215
|
+
* seam emits the templated `.after` events itself, so announcing beside it
|
|
216
|
+
* would double every subscriber's reaction; `paymentOrderStatusRegistry` goes
|
|
217
|
+
* because "is this a status code" was never the question the ingress needed
|
|
218
|
+
* answered, and the port answers the real one — may this order go there —
|
|
219
|
+
* against the configured graph.
|
|
220
|
+
*/
|
|
221
|
+
ctx.di.providePort('receivePaymentHandler', ctx
|
|
222
|
+
.asFunction(({ emFactory, eventBus }) => new ReceivePaymentHandler(emFactory, lazyPort(ctx, 'paymentMethodReadPort'), lazyPort(ctx, 'orderTransitionPort'), lazyPort(ctx, 'orderPaymentStatusApplyPort'), ctx.log, eventBus))
|
|
223
|
+
.singleton());
|
|
224
|
+
ctx.subscribe('payment.received.v1', async (payload) => {
|
|
225
|
+
const { orderId } = payload;
|
|
226
|
+
await ctx.cradle().paymentEmailNotifier.notify(orderId, 'paid', null);
|
|
227
|
+
});
|
|
228
|
+
ctx.subscribe('payment.failed.v1', async (payload) => {
|
|
229
|
+
const { orderId, failureReason } = payload;
|
|
230
|
+
await ctx
|
|
231
|
+
.cradle()
|
|
232
|
+
.paymentEmailNotifier.notify(orderId, 'failed', failureReason ?? null);
|
|
233
|
+
});
|
|
234
|
+
ctx.routes(async (app) => {
|
|
235
|
+
const { requireAdmin } = ctx.cradle();
|
|
236
|
+
await registerPaymentsRoutes(app, {
|
|
237
|
+
requireAdmin,
|
|
238
|
+
// Lazily, even though the module owns both ports: route *registration*
|
|
239
|
+
// runs inside `buildServer` whatever the module's effective state is, so
|
|
240
|
+
// destructuring the gates here would stop the next start instead of
|
|
241
|
+
// stopping the routes (D-40).
|
|
242
|
+
receiveHandler: lazyPort(ctx, 'receivePaymentHandler'),
|
|
243
|
+
paymentService: lazyPort(ctx, 'paymentService'),
|
|
244
|
+
});
|
|
245
|
+
await registerPaymentsCustomerRoutes(app, {
|
|
246
|
+
requireCustomer: (req, reply) => ctx.cradle().requireCustomer(req, reply),
|
|
247
|
+
resolveCustomerAccountId: (req) => ctx.cradle().customerAccountIdResolver(req),
|
|
248
|
+
// Resolvable here, unlike the two above: this is a plain registration
|
|
249
|
+
// rather than a gate, and every port it holds is lazy.
|
|
250
|
+
retryService: ctx.cradle().paymentRetryService,
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
/**
|
|
254
|
+
* The default subject and content for the 1 transactional email this
|
|
255
|
+
* module declares in its manifest (T143a).
|
|
256
|
+
*
|
|
257
|
+
* These were fourteen `emailDefaultsRegistry.register(...)` calls in
|
|
258
|
+
* `composition.ts`, each importing a template constant out of the module that
|
|
259
|
+
* owns it — a root reaching into seven modules to hand their own content to
|
|
260
|
+
* an eighth. Each module registers its own now.
|
|
261
|
+
*
|
|
262
|
+
* `ctx.onBoot` rather than a registration: the registry is *read* once, by
|
|
263
|
+
* `transactional_emails`' boot reconciler inside its plugin body. Boot hooks
|
|
264
|
+
* run during composition and plugin bodies only when the Fastify app is
|
|
265
|
+
* built, so this always lands first — by construction, not by ordering luck.
|
|
266
|
+
*/
|
|
267
|
+
ctx.onBoot(async () => {
|
|
268
|
+
const defaults = lazyPort(ctx, 'emailDefaultsPort');
|
|
269
|
+
defaults.register('payment_status_changed', PAYMENT_STATUS_CHANGED_DEFAULT, 'payments');
|
|
270
|
+
});
|
|
271
|
+
/**
|
|
272
|
+
* The four payment kinds this module implements, pushed into the registry
|
|
273
|
+
* `payment_methods` holds (T143a).
|
|
274
|
+
*
|
|
275
|
+
* `ctx.onBoot` rather than a registration, for the reason the other
|
|
276
|
+
* push-at-boot contributions give: the registry is *read* — by the
|
|
277
|
+
* payment-method admin surface, by storefront eligibility and by
|
|
278
|
+
* order placement — and a registration declares without resolving.
|
|
279
|
+
*
|
|
280
|
+
* It belongs in this module rather than in a root even though the registry is
|
|
281
|
+
* a deliberate **process** singleton (`payment_methods/services/registry-singleton.ts`,
|
|
282
|
+
* which the four gateway modules import directly from their install hooks).
|
|
283
|
+
* The singleton decides *where* a descriptor lands, not *who* may declare
|
|
284
|
+
* one, and this module owns the adapter classes.
|
|
285
|
+
*
|
|
286
|
+
* Each one names this module as it lands (issue #96): the registry filters
|
|
287
|
+
* its enumeration on the contributing module's effective state, so an entry
|
|
288
|
+
* that does not know who contributed it is an entry that keeps being offered
|
|
289
|
+
* to a buyer after an operator switches its owner off. Re-registration by the
|
|
290
|
+
* same owner is silent, which is what the old `isRegistered` guard was
|
|
291
|
+
* really for — the test suite performs several hundred compositions against
|
|
292
|
+
* this one process singleton.
|
|
293
|
+
*/
|
|
294
|
+
ctx.onBoot(() => {
|
|
295
|
+
const registry = ctx.cradle().paymentAdapterRegistry;
|
|
296
|
+
for (const adapter of builtInPaymentAdapters()) {
|
|
297
|
+
registry.register(adapter, 'payments');
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The module's persisted entity classes, on the `./backend` subpath, as one
|
|
303
|
+
* array and **no named class export** (D-168).
|
|
304
|
+
*
|
|
305
|
+
* This is the shape the platform reads when the package is *installed*: the
|
|
306
|
+
* boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
|
|
307
|
+
* and the static declaration reader (`scripts/lib/package-declarations.ts`),
|
|
308
|
+
* which is the third source of `check:module-boundary`'s `table->owner` map and
|
|
309
|
+
* the package pass of `check-entity-tenant-classification`. A missing array is
|
|
310
|
+
* answered with `[]` — zero entities registered, no error anywhere.
|
|
311
|
+
*
|
|
312
|
+
* One class, and the one that used to cross a boundary: `orders` constructed
|
|
313
|
+
* `Payment` directly until T048, which was the last cross-module entity-class
|
|
314
|
+
* reach in the tree. What crosses now is `PaymentPlacementApplyPort`, and D-168
|
|
315
|
+
* leaves a stranger no supported spelling for the class itself.
|
|
316
|
+
*/
|
|
317
|
+
export const entities = [Payment];
|
|
318
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
|
2
|
+
import type { PaymentRetryService } from './services/payment-retry-service.js';
|
|
3
|
+
/**
|
|
4
|
+
* The buyer's own payment surface (issue #264).
|
|
5
|
+
*
|
|
6
|
+
* POST /api/v1/orders/:orderId/payments/retry — pay an unpaid order again
|
|
7
|
+
*
|
|
8
|
+
* It sits in this module rather than in `orders` because the attempt row and
|
|
9
|
+
* the adapter dispatch are this module's, and because `payments` already
|
|
10
|
+
* declares `orders` — the reverse edge would close a cycle. It is the customer
|
|
11
|
+
* twin of `POST /api/v1/admin/orders/:id/payments/retry`, and both run through
|
|
12
|
+
* the same `PaymentService.openRetry`.
|
|
13
|
+
*/
|
|
14
|
+
export interface PaymentsCustomerRoutesDeps {
|
|
15
|
+
requireCustomer: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
16
|
+
resolveCustomerAccountId: (req: FastifyRequest) => string;
|
|
17
|
+
retryService: PaymentRetryService;
|
|
18
|
+
}
|
|
19
|
+
export declare function registerPaymentsCustomerRoutes(app: FastifyInstance, deps: PaymentsCustomerRoutesDeps): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=routes.customer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.customer.d.ts","sourceRoot":"","sources":["../../src/backend/routes.customer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,MAAM,WAAW,0BAA0B;IACzC,eAAe,EAAE,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,wBAAwB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,MAAM,CAAC;IAC1D,YAAY,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAsB,8BAA8B,CAClD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,0BAA0B,GAC/B,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ERROR_CODES, OrganizationCannotTransactError } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
export async function registerPaymentsCustomerRoutes(app, deps) {
|
|
4
|
+
app.post('/api/v1/orders/:orderId/payments/retry', { preHandler: deps.requireCustomer }, async (request, reply) => {
|
|
5
|
+
const callerId = deps.resolveCustomerAccountId(request);
|
|
6
|
+
try {
|
|
7
|
+
const result = await deps.retryService.retryForCustomer({
|
|
8
|
+
orderId: request.params.orderId,
|
|
9
|
+
customerAccountId: callerId,
|
|
10
|
+
});
|
|
11
|
+
return reply.send({ data: result });
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
// The same mapping placement uses for the same refusal, so a buyer of a
|
|
15
|
+
// suspended Organization is told the same thing whether they are
|
|
16
|
+
// placing an order or paying one.
|
|
17
|
+
if (err instanceof OrganizationCannotTransactError) {
|
|
18
|
+
throw new HttpError(423, ERROR_CODES.FORBIDDEN, 'Your Organization cannot transact in its current status.', { code: 'organization_cannot_transact', status: err.status });
|
|
19
|
+
}
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=routes.customer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.customer.js","sourceRoot":"","sources":["../../src/backend/routes.customer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,+BAA+B,EAAE,MAAM,4BAA4B,CAAC;AAC1F,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAoB3D,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,GAAoB,EACpB,IAAgC;IAEhC,GAAG,CAAC,IAAI,CACN,wCAAwC,EACxC,EAAE,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,EACpC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,wBAAwB,CAAC,OAAO,CAAC,CAAC;QACxD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAAC;gBACtD,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO;gBAC/B,iBAAiB,EAAE,QAAQ;aAC5B,CAAC,CAAC;YACH,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,wEAAwE;YACxE,iEAAiE;YACjE,kCAAkC;YAClC,IAAI,GAAG,YAAY,+BAA+B,EAAE,CAAC;gBACnD,MAAM,IAAI,SAAS,CACjB,GAAG,EACH,WAAW,CAAC,SAAS,EACrB,0DAA0D,EAC1D,EAAE,IAAI,EAAE,8BAA8B,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAC7D,CAAC;YACJ,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { ReceivePaymentHandler } from './services/receive-payment-handler.js';
|
|
3
|
+
import type { PaymentService } from './services/payment-service.js';
|
|
4
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
5
|
+
/**
|
|
6
|
+
* Payments routes (feature 034).
|
|
7
|
+
*
|
|
8
|
+
* POST /api/v1/payments/receive — receive_payment ingress (FR-022)
|
|
9
|
+
* POST /api/v1/admin/orders/:id/payments/retry — open a retry Payment (FR-024)
|
|
10
|
+
* GET /api/v1/admin/orders/:id/payments — full payment history (FR-026)
|
|
11
|
+
*
|
|
12
|
+
* The buyer's own retry is a separate file (`routes.customer.ts`, issue #264):
|
|
13
|
+
* it authorises differently, guards the Organization's ability to transact, and
|
|
14
|
+
* starts the gateway session this one deliberately does not.
|
|
15
|
+
*
|
|
16
|
+
* The ingress is admin-guarded for the MVP; a signed PSP-webhook auth path is a
|
|
17
|
+
* follow-up (the offline reference adapters are settled by an admin anyway).
|
|
18
|
+
*
|
|
19
|
+
* All three gate on this module's own codes. They used to gate on `catalog:read`
|
|
20
|
+
* and `catalog:write`, which meant an operator who could edit a product could
|
|
21
|
+
* read every payment's provider payload and declare an arbitrary payment
|
|
22
|
+
* settled. The reasoning for the pair, and for there being no third code for the
|
|
23
|
+
* ingress, is in `manifest.ts` beside the declaration.
|
|
24
|
+
*
|
|
25
|
+
* The ingress body is `receivePaymentRequestSchema` rather than
|
|
26
|
+
* `receivePaymentSchema`: the two differ only in `providerDetails`, which is
|
|
27
|
+
* bounded for the operator-supplied path and left open for the gateway modules
|
|
28
|
+
* that build the payload in code.
|
|
29
|
+
*/
|
|
30
|
+
export interface PaymentsRoutesDeps {
|
|
31
|
+
requireAdmin: RequireAdminFactory;
|
|
32
|
+
receiveHandler: ReceivePaymentHandler;
|
|
33
|
+
paymentService: PaymentService;
|
|
34
|
+
}
|
|
35
|
+
export declare function registerPaymentsRoutes(app: FastifyInstance, deps: PaymentsRoutesDeps): Promise<void>;
|
|
36
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AACnF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,mBAAmB,CAAC;IAClC,cAAc,EAAE,qBAAqB,CAAC;IACtC,cAAc,EAAE,cAAc,CAAC;CAChC;AAED,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,IAAI,CAAC,CAoCf"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { receivePaymentRequestSchema } from '@endora-commerce/contracts';
|
|
2
|
+
export async function registerPaymentsRoutes(app, deps) {
|
|
3
|
+
app.post('/api/v1/payments/receive', {
|
|
4
|
+
preHandler: deps.requireAdmin('payments:write'),
|
|
5
|
+
schema: { body: receivePaymentRequestSchema },
|
|
6
|
+
}, async (request) => {
|
|
7
|
+
const body = receivePaymentRequestSchema.parse(request.body);
|
|
8
|
+
const result = await deps.receiveHandler.receive(body);
|
|
9
|
+
return { data: result };
|
|
10
|
+
});
|
|
11
|
+
app.post('/api/v1/admin/orders/:id/payments/retry', { preHandler: deps.requireAdmin('payments:write') }, async (request, reply) => {
|
|
12
|
+
// `opened` is discarded here on purpose: the operator asked for the next
|
|
13
|
+
// attempt and gets it, whether it had to be created or was already open.
|
|
14
|
+
// The buyer-facing twin routes on that difference — see
|
|
15
|
+
// `routes.customer.ts` — because only it starts a provider session.
|
|
16
|
+
const { payment } = await deps.paymentService.openRetry(request.params.id);
|
|
17
|
+
reply.status(201);
|
|
18
|
+
return { data: serializePayment(payment) };
|
|
19
|
+
});
|
|
20
|
+
app.get('/api/v1/admin/orders/:id/payments', { preHandler: deps.requireAdmin('payments:read') }, async (request) => {
|
|
21
|
+
const payments = await deps.paymentService.listForOrder(request.params.id);
|
|
22
|
+
return { data: payments.map(serializePayment) };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function serializePayment(p) {
|
|
26
|
+
return {
|
|
27
|
+
id: p.id,
|
|
28
|
+
orderId: p.orderId,
|
|
29
|
+
paymentMethodId: p.paymentMethodId,
|
|
30
|
+
status: p.status,
|
|
31
|
+
amount: Number(p.amount),
|
|
32
|
+
refundedAmount: Number(p.refundedAmount ?? '0'),
|
|
33
|
+
refundedAt: p.providerDetails?.['refundedAt'] ?? null,
|
|
34
|
+
refundReference: p.providerDetails?.['refundReference'] ?? null,
|
|
35
|
+
updatedAt: p.updatedAt ? p.updatedAt.toISOString() : null,
|
|
36
|
+
currency: p.currency,
|
|
37
|
+
paidAt: p.paidAt ? p.paidAt.toISOString() : null,
|
|
38
|
+
externalReference: p.externalReference ?? null,
|
|
39
|
+
providerDetails: p.providerDetails ?? null,
|
|
40
|
+
failureReason: p.failureReason ?? null,
|
|
41
|
+
attemptNo: p.attemptNo,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAoCzE,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,GAAoB,EACpB,IAAwB;IAExB,GAAG,CAAC,IAAI,CACN,0BAA0B,EAC1B;QACE,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAAC;QAC/C,MAAM,EAAE,EAAE,IAAI,EAAE,2BAA2B,EAAE;KAC9C,EACD,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,2BAA2B,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1B,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,IAAI,CACN,yCAAyC,EACzC,EAAE,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAAC,EAAE,EACnD,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,yEAAyE;QACzE,yEAAyE;QACzE,wDAAwD;QACxD,oEAAoE;QACpE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3E,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAClB,OAAO,EAAE,IAAI,EAAE,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC;IAC7C,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,mCAAmC,EACnC,EAAE,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,EAAE,EAClD,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3E,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC;IAClD,CAAC,CACF,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,CAczB;IACC,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,eAAe,EAAE,CAAC,CAAC,eAAe;QAClC,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,cAAc,IAAI,GAAG,CAAC;QAC/C,UAAU,EAAG,CAAC,CAAC,eAAe,EAAE,CAAC,YAAY,CAAwB,IAAI,IAAI;QAC7E,eAAe,EAAG,CAAC,CAAC,eAAe,EAAE,CAAC,iBAAiB,CAAwB,IAAI,IAAI;QACvF,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;QACzD,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;QAChD,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,IAAI,IAAI;QAC9C,eAAe,EAAE,CAAC,CAAC,eAAe,IAAI,IAAI;QAC1C,aAAa,EAAE,CAAC,CAAC,aAAa,IAAI,IAAI;QACtC,SAAS,EAAE,CAAC,CAAC,SAAS;KACvB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { PaymentRefundInput, PaymentRefundResult } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* GatewayRefundRegistry (feature 049) — the generic seam through which a PSP
|
|
4
|
+
* vendor module (e.g. Stripe) fulfils refunds for `kind === 'gateway'` payments.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the PaymentAdapterRegistry pattern: a vendor module imports this
|
|
7
|
+
* process-wide singleton and registers its refund handler from its boot hook,
|
|
8
|
+
* so the platform has exactly one table of handlers however many times it is
|
|
9
|
+
* composed. `PaymentRefundProvider` consults it for gateway payments and falls
|
|
10
|
+
* back to `pending_manual` when no handler answers — keeping the `payments` and
|
|
11
|
+
* `returns` modules provider-agnostic.
|
|
12
|
+
*
|
|
13
|
+
* **Every entry names the module that contributed it (feature 074, FR-024).**
|
|
14
|
+
* This is a contribution registry in D-39's sense: the push is ungated — a boot
|
|
15
|
+
* hook runs whatever the contributing module's effective state is, and gating
|
|
16
|
+
* the push would make a deactivation survive as a permanently missing entry —
|
|
17
|
+
* so the presence question is answered *here*, at enumeration, keyed on the
|
|
18
|
+
* owner recorded with the entry. Until this shipped the registry recorded no
|
|
19
|
+
* owner and stated no policy, and D-44 §7 named it the one live instance of
|
|
20
|
+
* that gap: a switched-off gateway went on refunding through its own PSP API.
|
|
21
|
+
*
|
|
22
|
+
* **The policy this registry states: skip an absent owner's handler, and say
|
|
23
|
+
* so (D-71).** `get`, `resolve` and `list` answer as if it were not registered;
|
|
24
|
+
* `entry`, `ownerOf`, {@link GatewayRefundRegistry.absentOwnerFor} and `listAll`
|
|
25
|
+
* deliberately do not filter, because a settlement screen has to keep showing
|
|
26
|
+
* the handler *and* the reason it is unavailable — and because the caller has to
|
|
27
|
+
* be able to tell "switched off" from "never installed", which one empty
|
|
28
|
+
* `resolve` cannot. Three things decide the skip, and the money is on the other
|
|
29
|
+
* side of each:
|
|
30
|
+
*
|
|
31
|
+
* 1. A module that is off must not act. Refunding through a PSP the operator
|
|
32
|
+
* switched off charges that PSP's API with that operator's credentials —
|
|
33
|
+
* the opposite of "behaves as if never installed" (Principle XVII).
|
|
34
|
+
* 2. **The obligation is not dropped with the handler.** The caller has to say
|
|
35
|
+
* what happens next, and the two situations it can be in are not the same
|
|
36
|
+
* one: a deployment that never installed a PSP integration records
|
|
37
|
+
* `pending_manual` and settles by hand, while a gateway an operator switched
|
|
38
|
+
* off is a capability that is *supposed* to be there and can be back in one
|
|
39
|
+
* click. D-71 rules the second a **refusal**, not an outcome — see
|
|
40
|
+
* `PaymentRefundProvider`.
|
|
41
|
+
* 3. {@link resolve} answers with the sole registered handler when the order
|
|
42
|
+
* names no adapter. Honouring an absent owner would route exactly those
|
|
43
|
+
* refunds — the ones with the least information behind them — into a
|
|
44
|
+
* switched-off gateway.
|
|
45
|
+
*
|
|
46
|
+
* Feature 075's Phase C took the cut Phase P set up: the two shapes `returns`
|
|
47
|
+
* declares are read from `@endora-commerce/contracts` now, so this file names no other
|
|
48
|
+
* module. The declaration itself stays here rather than being replaced by the
|
|
49
|
+
* package's structurally identical `GatewayRefundRegistryPort`, and the reason
|
|
50
|
+
* is `absentOwnerFor`: the published port is the **contribution** surface the
|
|
51
|
+
* gateways push into, and it deliberately does not carry the question D-71
|
|
52
|
+
* added — "which module *would* have handled this, but is switched off". That
|
|
53
|
+
* question is what keeps a refusal apart from `pending_manual`, and it is asked
|
|
54
|
+
* by this module's own `PaymentRefundProvider` over this module's own registry,
|
|
55
|
+
* which is not a cross-module call at all.
|
|
56
|
+
*/
|
|
57
|
+
export interface GatewayRefundHandler {
|
|
58
|
+
/** The payment adapter key this handler serves (e.g. `stripe`). */
|
|
59
|
+
readonly adapterKey: string;
|
|
60
|
+
refund(input: PaymentRefundInput): Promise<PaymentRefundResult>;
|
|
61
|
+
}
|
|
62
|
+
export interface RegistryLogger {
|
|
63
|
+
warn(message: string): void;
|
|
64
|
+
}
|
|
65
|
+
/** One contributed handler, with the module that contributed it. */
|
|
66
|
+
export interface GatewayRefundEntry {
|
|
67
|
+
readonly handler: GatewayRefundHandler;
|
|
68
|
+
readonly module: string;
|
|
69
|
+
}
|
|
70
|
+
export declare class GatewayRefundRegistry {
|
|
71
|
+
private readonly log;
|
|
72
|
+
private readonly isModulePresent;
|
|
73
|
+
private readonly handlers;
|
|
74
|
+
/**
|
|
75
|
+
* @param log collision warnings.
|
|
76
|
+
* @param isModulePresent the effective-state probe. Defaults to
|
|
77
|
+
* always-present, so a registry a unit test builds for itself keeps
|
|
78
|
+
* answering about the handlers that test registered; the process singleton
|
|
79
|
+
* wires it to the kernel's effective state.
|
|
80
|
+
*/
|
|
81
|
+
constructor(log?: RegistryLogger, isModulePresent?: (moduleId: string) => boolean);
|
|
82
|
+
register(handler: GatewayRefundHandler, moduleId: string): void;
|
|
83
|
+
unregister(adapterKey: string): void;
|
|
84
|
+
/** The contributed entry, presence-blind. Diagnostics read this. */
|
|
85
|
+
entry(adapterKey: string): GatewayRefundEntry | undefined;
|
|
86
|
+
/** The module that contributed `adapterKey`, or `null` when nobody did. */
|
|
87
|
+
ownerOf(adapterKey: string): string | null;
|
|
88
|
+
/**
|
|
89
|
+
* The module that would have handled this refund but is not present (D-71).
|
|
90
|
+
*
|
|
91
|
+
* The question a caller asks *after* {@link resolve} declined, because one
|
|
92
|
+
* empty `resolve` covers two situations that are not interchangeable: a
|
|
93
|
+
* gateway an operator switched off is a capability restorable in one click
|
|
94
|
+
* and must be **refused**, while an adapter nobody ever registered is a
|
|
95
|
+
* deployment with no PSP integration, which records the obligation and
|
|
96
|
+
* settles by hand.
|
|
97
|
+
*
|
|
98
|
+
* It mirrors `resolve`'s two arms exactly, which is why the unkeyed one is
|
|
99
|
+
* here at all: `resolve` answers with the sole registered handler when the
|
|
100
|
+
* order names no adapter, and point 3 of the docblock above says that is the
|
|
101
|
+
* worst arm to get wrong — the refunds with the least information behind
|
|
102
|
+
* them. Presence-blind in the same sense `ownerOf` is.
|
|
103
|
+
*/
|
|
104
|
+
absentOwnerFor(adapterKey?: string | null): string | null;
|
|
105
|
+
/** The handler, or `undefined` when unregistered or its owner is absent. */
|
|
106
|
+
get(adapterKey: string): GatewayRefundHandler | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the handler for a gateway payment.
|
|
109
|
+
* - Known `adapterKey` → exact handler only (never guess another PSP).
|
|
110
|
+
* - Missing key → sole registered handler when exactly one exists.
|
|
111
|
+
*
|
|
112
|
+
* Both arms drop a handler whose owner is absent, and the second arm counts
|
|
113
|
+
* only the handlers that are available: with one gateway installed and
|
|
114
|
+
* switched off, "the sole registered handler" must not be it.
|
|
115
|
+
*/
|
|
116
|
+
resolve(adapterKey?: string | null): GatewayRefundHandler | undefined;
|
|
117
|
+
/** Adapter keys whose owner is present (stable insertion order). */
|
|
118
|
+
list(): string[];
|
|
119
|
+
/** Every registered adapter key, presence-blind. */
|
|
120
|
+
listAll(): string[];
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=gateway-refund-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway-refund-registry.d.ts","sourceRoot":"","sources":["../../../src/backend/services/gateway-refund-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAE1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACjE;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAMD,oEAAoE;AACpE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,qBAAqB;IAW9B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,eAAe;IAXlC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyC;IAElE;;;;;;OAMG;gBAEgB,GAAG,GAAE,cAA8B,EACnC,eAAe,GAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAoB;IAG9E,QAAQ,CAAC,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAW/D,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIpC,oEAAoE;IACpE,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIzD,2EAA2E;IAC3E,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAI1C;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI;IAczD,4EAA4E;IAC5E,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS;IAMzD;;;;;;;;OAQG;IACH,OAAO,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,oBAAoB,GAAG,SAAS;IAUrE,oEAAoE;IACpE,IAAI,IAAI,MAAM,EAAE;IAMhB,oDAAoD;IACpD,OAAO,IAAI,MAAM,EAAE;CAGpB"}
|