@endora-commerce/mod-payments 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/admin/index.d.ts +32 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +61 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/zones/OrderPaymentsTab.d.ts +42 -0
  8. package/dist/admin/zones/OrderPaymentsTab.d.ts.map +1 -0
  9. package/dist/admin/zones/OrderPaymentsTab.js +104 -0
  10. package/dist/admin/zones/OrderPaymentsTab.js.map +1 -0
  11. package/dist/backend/adapters/built-in-adapters.d.ts +58 -0
  12. package/dist/backend/adapters/built-in-adapters.d.ts.map +1 -0
  13. package/dist/backend/adapters/built-in-adapters.js +83 -0
  14. package/dist/backend/adapters/built-in-adapters.js.map +1 -0
  15. package/dist/backend/drivers/bank-transfer-driver.d.ts +28 -0
  16. package/dist/backend/drivers/bank-transfer-driver.d.ts.map +1 -0
  17. package/dist/backend/drivers/bank-transfer-driver.js +26 -0
  18. package/dist/backend/drivers/bank-transfer-driver.js.map +1 -0
  19. package/dist/backend/drivers/gateway-adapter-port.d.ts +71 -0
  20. package/dist/backend/drivers/gateway-adapter-port.d.ts.map +1 -0
  21. package/dist/backend/drivers/gateway-adapter-port.js +18 -0
  22. package/dist/backend/drivers/gateway-adapter-port.js.map +1 -0
  23. package/dist/backend/drivers/pickup-driver.d.ts +23 -0
  24. package/dist/backend/drivers/pickup-driver.d.ts.map +1 -0
  25. package/dist/backend/drivers/pickup-driver.js +19 -0
  26. package/dist/backend/drivers/pickup-driver.js.map +1 -0
  27. package/dist/backend/email-templates/transactional-defaults.d.ts +6 -0
  28. package/dist/backend/email-templates/transactional-defaults.d.ts.map +1 -0
  29. package/dist/backend/email-templates/transactional-defaults.js +27 -0
  30. package/dist/backend/email-templates/transactional-defaults.js.map +1 -0
  31. package/dist/backend/entities/payment.entity.d.ts +26 -0
  32. package/dist/backend/entities/payment.entity.d.ts.map +1 -0
  33. package/dist/backend/entities/payment.entity.js +100 -0
  34. package/dist/backend/entities/payment.entity.js.map +1 -0
  35. package/dist/backend/index.d.ts +88 -0
  36. package/dist/backend/index.d.ts.map +1 -0
  37. package/dist/backend/index.js +318 -0
  38. package/dist/backend/index.js.map +1 -0
  39. package/dist/backend/routes.customer.d.ts +20 -0
  40. package/dist/backend/routes.customer.d.ts.map +1 -0
  41. package/dist/backend/routes.customer.js +24 -0
  42. package/dist/backend/routes.customer.js.map +1 -0
  43. package/dist/backend/routes.d.ts +36 -0
  44. package/dist/backend/routes.d.ts.map +1 -0
  45. package/dist/backend/routes.js +44 -0
  46. package/dist/backend/routes.js.map +1 -0
  47. package/dist/backend/services/gateway-refund-registry.d.ts +122 -0
  48. package/dist/backend/services/gateway-refund-registry.d.ts.map +1 -0
  49. package/dist/backend/services/gateway-refund-registry.js +106 -0
  50. package/dist/backend/services/gateway-refund-registry.js.map +1 -0
  51. package/dist/backend/services/payment-email-notifier.d.ts +71 -0
  52. package/dist/backend/services/payment-email-notifier.d.ts.map +1 -0
  53. package/dist/backend/services/payment-email-notifier.js +73 -0
  54. package/dist/backend/services/payment-email-notifier.js.map +1 -0
  55. package/dist/backend/services/payment-email-renderer.d.ts +22 -0
  56. package/dist/backend/services/payment-email-renderer.d.ts.map +1 -0
  57. package/dist/backend/services/payment-email-renderer.js +17 -0
  58. package/dist/backend/services/payment-email-renderer.js.map +1 -0
  59. package/dist/backend/services/payment-placement-apply-port.d.ts +33 -0
  60. package/dist/backend/services/payment-placement-apply-port.d.ts.map +1 -0
  61. package/dist/backend/services/payment-placement-apply-port.js +68 -0
  62. package/dist/backend/services/payment-placement-apply-port.js.map +1 -0
  63. package/dist/backend/services/payment-read-port.d.ts +33 -0
  64. package/dist/backend/services/payment-read-port.d.ts.map +1 -0
  65. package/dist/backend/services/payment-read-port.js +64 -0
  66. package/dist/backend/services/payment-read-port.js.map +1 -0
  67. package/dist/backend/services/payment-reference-port.d.ts +29 -0
  68. package/dist/backend/services/payment-reference-port.d.ts.map +1 -0
  69. package/dist/backend/services/payment-reference-port.js +70 -0
  70. package/dist/backend/services/payment-reference-port.js.map +1 -0
  71. package/dist/backend/services/payment-refund.d.ts +55 -0
  72. package/dist/backend/services/payment-refund.d.ts.map +1 -0
  73. package/dist/backend/services/payment-refund.js +101 -0
  74. package/dist/backend/services/payment-refund.js.map +1 -0
  75. package/dist/backend/services/payment-retry-service.d.ts +45 -0
  76. package/dist/backend/services/payment-retry-service.d.ts.map +1 -0
  77. package/dist/backend/services/payment-retry-service.js +187 -0
  78. package/dist/backend/services/payment-retry-service.js.map +1 -0
  79. package/dist/backend/services/payment-service.d.ts +62 -0
  80. package/dist/backend/services/payment-service.d.ts.map +1 -0
  81. package/dist/backend/services/payment-service.js +134 -0
  82. package/dist/backend/services/payment-service.js.map +1 -0
  83. package/dist/backend/services/receive-payment-handler.d.ts +260 -0
  84. package/dist/backend/services/receive-payment-handler.d.ts.map +1 -0
  85. package/dist/backend/services/receive-payment-handler.js +356 -0
  86. package/dist/backend/services/receive-payment-handler.js.map +1 -0
  87. package/dist/backend/services/registry-singleton.d.ts +16 -0
  88. package/dist/backend/services/registry-singleton.d.ts.map +1 -0
  89. package/dist/backend/services/registry-singleton.js +17 -0
  90. package/dist/backend/services/registry-singleton.js.map +1 -0
  91. package/dist/manifest.d.ts +210 -0
  92. package/dist/manifest.d.ts.map +1 -0
  93. package/dist/manifest.js +182 -0
  94. package/dist/manifest.js.map +1 -0
  95. package/dist/migrations/20260925T115728_payments_refunded_amount.d.ts +28 -0
  96. package/dist/migrations/20260925T115728_payments_refunded_amount.d.ts.map +1 -0
  97. package/dist/migrations/20260925T115728_payments_refunded_amount.js +32 -0
  98. package/dist/migrations/20260925T115728_payments_refunded_amount.js.map +1 -0
  99. package/dist/migrations/index.d.ts +27 -0
  100. package/dist/migrations/index.d.ts.map +1 -0
  101. package/dist/migrations/index.js +29 -0
  102. package/dist/migrations/index.js.map +1 -0
  103. package/docs/payments.md +69 -0
  104. package/i18n/en.json +5 -0
  105. package/i18n/pl.json +5 -0
  106. package/package.json +96 -0
  107. package/tailwind.css +14 -0
@@ -0,0 +1,134 @@
1
+ import { LockMode } from '@mikro-orm/core';
2
+ import { ERROR_CODES } from '@endora-commerce/contracts';
3
+ import { HttpError } from '@endora-commerce/platform/http';
4
+ import { Payment } from '../entities/payment.entity.js';
5
+ /**
6
+ * PaymentService (feature 034, FR-024) — opens retry Payments. The first
7
+ * Payment of an order is created by OrderService.placeOrder; a retry opens a
8
+ * new row against the same order with the next `attemptNo`, leaving prior
9
+ * (failed) attempts intact so the admin can see the full history.
10
+ *
11
+ * **A fresh attempt row is what makes a gateway able to start over**
12
+ * (issue #264), which is why the customer-facing retry runs through here rather
13
+ * than re-driving the failed row. Every gateway keys its own object on this
14
+ * platform's payment id: PayU sends it as `extOrderId`, which must be unique
15
+ * per POS; TPay resolves the newest `tpay_transactions` row for the order;
16
+ * Stripe records a `stripe_payment_intents` mapping per payment. Re-using the
17
+ * failed attempt asks all four to open a second session against an identifier
18
+ * they have already spent.
19
+ */
20
+ export class PaymentService {
21
+ emFactory;
22
+ orderRead;
23
+ constructor(emFactory, orderRead) {
24
+ this.emFactory = emFactory;
25
+ this.orderRead = orderRead;
26
+ }
27
+ /**
28
+ * Resolve the order every method below is keyed on, through its owner.
29
+ *
30
+ * `Payment` is `@GlobalEntity` — no organization column, no filter — so
31
+ * `em.find(Payment, { orderId })` answers for every order on the platform.
32
+ * `Order` is `@OrgScoped`, and `orderReadPort.findById` goes through a
33
+ * filtered EntityManager, so this read is the whole tenant boundary for an
34
+ * order-keyed payment operation: an assignment-scoped administrator gets
35
+ * `null` for an order outside their organizations and the caller stops here.
36
+ *
37
+ * It sits in the service rather than in the two admin route handlers on
38
+ * purpose. A route-level guard protects the routes that exist on the day it
39
+ * is written; this one protects the next caller too, and both of the routes
40
+ * that shipped without it were written by someone who had no reason to know
41
+ * the child carried no filter.
42
+ *
43
+ * The buyer's own retry (`PaymentRetryService`) reads and authorises the
44
+ * order before it gets here, so for that path this is a second read of a row
45
+ * the identity map already holds — deliberately not optimised away, because
46
+ * the guarantee must not depend on which caller arrives.
47
+ *
48
+ * 404 rather than 403, matching the order-keyed routes in `orders`: an
49
+ * out-of-scope order must not be distinguishable from one that is not there.
50
+ */
51
+ async #assertOrderInScope(orderId) {
52
+ const order = await this.orderRead.findById(orderId);
53
+ if (!order) {
54
+ throw new HttpError(404, ERROR_CODES.ORDER_NOT_FOUND, 'Order not found.');
55
+ }
56
+ }
57
+ /**
58
+ * The next attempt for `orderId`, opening one only when the latest has
59
+ * failed.
60
+ *
61
+ * The latest attempt is read `FOR UPDATE`, so two callers racing on the same
62
+ * order serialise here and the second sees what the first wrote: a buyer
63
+ * double-clicking gets one new attempt, not two. It is the same row the
64
+ * settlement ingress resolves, so a retry and a late gateway callback cannot
65
+ * interleave halfway through each other.
66
+ */
67
+ async openRetry(orderId) {
68
+ // command-coverage-ignore: opens a new payment attempt after a failure —
69
+ // checkout retry mechanics; the order/payment status transition is audited in
70
+ // the orders flow.
71
+ await this.#assertOrderInScope(orderId);
72
+ const em = this.emFactory();
73
+ return em.transactional(async (tx) => {
74
+ const latest = await tx.findOne(Payment, { orderId }, { orderBy: { attemptNo: 'desc' }, lockMode: LockMode.PESSIMISTIC_WRITE });
75
+ if (!latest) {
76
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'No payment exists for this order to retry.');
77
+ }
78
+ if (latest.status === 'paid') {
79
+ throw new HttpError(409, ERROR_CODES.VALIDATION_FAILED, 'Order is already paid; nothing to retry.');
80
+ }
81
+ if (latest.status === 'refunded' || latest.status === 'partially_refunded') {
82
+ throw new HttpError(409, ERROR_CODES.VALIDATION_FAILED, 'This payment has been refunded; it cannot be retried.');
83
+ }
84
+ if (latest.status === 'deferred') {
85
+ throw new HttpError(409, ERROR_CODES.VALIDATION_FAILED, 'This order is settled out of band; there is nothing to retry.');
86
+ }
87
+ // Still open: hand back the attempt that exists rather than opening a
88
+ // second one beside it. Two live attempts for one order is two provider
89
+ // objects that can both settle.
90
+ if (latest.status === 'awaiting_payment') {
91
+ return { payment: latest, opened: false };
92
+ }
93
+ const next = tx.create(Payment, {
94
+ orderId,
95
+ paymentMethodId: latest.paymentMethodId,
96
+ amount: latest.amount,
97
+ currency: latest.currency,
98
+ attemptNo: latest.attemptNo + 1,
99
+ });
100
+ await tx.persistAndFlush(next);
101
+ return { payment: next, opened: true };
102
+ });
103
+ }
104
+ /**
105
+ * Close an attempt whose provider session could not be started.
106
+ *
107
+ * The compensating half of {@link openRetry}: the row is written before the
108
+ * adapter is asked (the adapter needs its id), so an adapter that throws
109
+ * would otherwise leave an `awaiting_payment` attempt no provider knows
110
+ * about — and the next retry would *resume* that phantom instead of opening a
111
+ * real one. Marking it failed puts the order back where the buyer can try
112
+ * again.
113
+ */
114
+ async failAttempt(paymentId, reason) {
115
+ // command-coverage-ignore: closes an attempt whose provider session never
116
+ // started, so the buyer's next retry opens a real one — the same retry
117
+ // mechanics `openRetry` above is exempted for.
118
+ const em = this.emFactory();
119
+ await em.transactional(async (tx) => {
120
+ const payment = await tx.findOne(Payment, { id: paymentId });
121
+ if (!payment || payment.status !== 'awaiting_payment')
122
+ return;
123
+ payment.status = 'failed';
124
+ payment.failureReason = reason;
125
+ await tx.flush();
126
+ });
127
+ }
128
+ async listForOrder(orderId) {
129
+ await this.#assertOrderInScope(orderId);
130
+ const em = this.emFactory();
131
+ return em.find(Payment, { orderId }, { orderBy: { attemptNo: 'asc' } });
132
+ }
133
+ }
134
+ //# sourceMappingURL=payment-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-service.js","sourceRoot":"","sources":["../../../src/backend/services/payment-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,260 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type ReceivePayment } from '@endora-commerce/contracts';
3
+ import type { OrderTransitionPort, PaymentMethodReadPort } from '@endora-commerce/contracts';
4
+ import type { EventBase, EventBus } from '@endora-commerce/platform/events';
5
+ import { Payment } from '../entities/payment.entity.js';
6
+ /**
7
+ * The one cross-module type this handler names, and the seam feature 075 kept
8
+ * here **permanently** (D-78 point 2) — now expressed as the owner's own
9
+ * published interface rather than as its entity class (feature 080, T048;
10
+ * D-169).
11
+ *
12
+ * `payments.order_id` carries a declared foreign key into `orders.id`
13
+ * (`payments_order_fk`, `on delete restrict`), so this is a genuinely
14
+ * co-transactional seam: a gateway callback moves the payment row and the
15
+ * order's `paymentStatus` in one `em.transactional`, and either both land or
16
+ * neither does. D-78 rules that such a seam keeps the caller's `EntityManager`
17
+ * and is *declared* — `orders` is in this module's manifest `dependencies` (the
18
+ * FK already required it), the ledger entry names the constraint, and this
19
+ * comment says which transaction the write runs in.
20
+ *
21
+ * **What T048 changed is who writes the statement, and nothing else.** It used
22
+ * to be `tx.findOne(Order, …)` here, followed by an assignment onto the managed
23
+ * entity — this module holding another module's aggregate open on a transaction
24
+ * it controls, free to move any column of it. It is now
25
+ * `orderPaymentStatusApplyPort.applyPaymentStatus(tx, …)`: the same transaction,
26
+ * the same constraint, the same one column, written by the module that owns the
27
+ * table and answered with a published record. D-168 leaves a packaged `orders`
28
+ * no entity class for a stranger to name, which is why the conversion had to
29
+ * happen before `orders` moves; the foreign key needs the **table** and never
30
+ * the class (D-169), so `payments_order_fk` is untouched.
31
+ *
32
+ * **The lifecycle half of that seam is gone since feature 085 Phase D**, and
33
+ * the ruling is unaffected by its going. `order.status` was written here too,
34
+ * on the same `tx` — a *transition*, with a graph, guards, an audit entry and
35
+ * side-effects, none of which a column assignment performs. It moved to
36
+ * `orderTransitionPort`, called after this transaction has committed. What is
37
+ * left in the transaction is the money axis, `order.paymentStatus`, which is
38
+ * the column `payments_order_fk` genuinely holds together with the payment row.
39
+ *
40
+ * **It is the only import left, and the other two went because their blocker
41
+ * did.** They were held open by a sentence that had stopped being true: that
42
+ * `stripe`, `payu`, `tpay` and `autopay` each construct this handler
43
+ * themselves, so its constructor could not take a port none of them can build.
44
+ * All four resolve `receivePaymentPort` today and the one construction left is
45
+ * in `payments/backend.ts`, where `ctx` is in hand — so the payment-method read
46
+ * is `paymentMethodReadPort` and the lifecycle write is `orderTransitionPort`.
47
+ * Neither shares the constraint above: the method read is a read of a row this
48
+ * transaction never writes (no FK obliges it to be co-transactional, and the
49
+ * identical read is a port in `shipments`' twin handler), and the transition
50
+ * runs **after** the commit, for the reason the port's own contract gives.
51
+ */
52
+ import type { OrderPaymentStatusApplyPort } from '@endora-commerce/mod-orders/ports';
53
+ /**
54
+ * Re-exported so `backend.ts` names its own module for the same type. One seam,
55
+ * one ledger entry: a second import specifier in the composition file would be
56
+ * a second crossing of a boundary that has exactly one reason to be crossed,
57
+ * and the reason is stated above. `orders` does the same for the two
58
+ * `EntityManager`-taking interfaces it reads.
59
+ */
60
+ export type { OrderPaymentStatusApplyPort };
61
+ export interface PaymentEvents extends Record<string, EventBase> {
62
+ 'payment.received.v1': EventBase & {
63
+ orderId: string;
64
+ paymentId: string;
65
+ adapter: string;
66
+ externalReference: string | null;
67
+ attemptNo: number;
68
+ };
69
+ 'payment.failed.v1': EventBase & {
70
+ orderId: string;
71
+ paymentId: string;
72
+ adapter: string;
73
+ failureReason: string | null;
74
+ attemptNo: number;
75
+ };
76
+ 'payment.refunded.v1': EventBase & {
77
+ orderId: string;
78
+ paymentId: string;
79
+ refundedAmount: number;
80
+ currency: string;
81
+ fullyRefunded: boolean;
82
+ externalRefundId: string | null;
83
+ };
84
+ }
85
+ export type PaymentEventBus = EventBus<PaymentEvents>;
86
+ export interface ReceivePaymentResult {
87
+ paymentId: string;
88
+ status: Payment['status'];
89
+ /**
90
+ * Where the order stands once the ingress is done with it, which since
91
+ * feature 085 Phase D is the lifecycle's answer rather than this handler's:
92
+ * the target the method configured when the graph permitted it, the order's
93
+ * unchanged status when it did not, and `null` when the ingress asked for no
94
+ * move at all (a repeated settlement, or a method with no status configured).
95
+ */
96
+ orderStatus: string | null;
97
+ idempotent: boolean;
98
+ }
99
+ /**
100
+ * The one message this handler logs, as little of a logger as it needs.
101
+ *
102
+ * `ctx.log` satisfies it structurally, so `payments/backend.ts` passes the
103
+ * module's own logger and a test passes a recorder. Declared here rather than
104
+ * imported so the handler carries no opinion about who is logging — the same
105
+ * shape `GatewayRefundRegistry` takes for its collision warning.
106
+ */
107
+ export interface SettlementLogger {
108
+ warn(details: object, message: string): void;
109
+ }
110
+ /**
111
+ * ReceivePaymentHandler (feature 034, FR-022/FR-023/FR-025).
112
+ *
113
+ * Resolves a Payment from the ingress payload, applies the outcome, and asks
114
+ * the order lifecycle for the move the method's `statusOnSuccess` /
115
+ * `statusOnFailure` names. Idempotent: a success after a terminal `paid` is a
116
+ * no-op; a failure after `paid` is rejected (no downgrade). Resolves a late
117
+ * event even when the adapter has since been de-registered (it keys on the
118
+ * persisted Payment, not the live registry).
119
+ *
120
+ * **The lifecycle answers; this handler does not decide** (feature 085 Phase
121
+ * D). It used to assign `order.status` after asking an `OrderStatusRegistry`
122
+ * whether the code existed, which is a different question from whether the
123
+ * order may go there — so the ingress wrote transitions the configured graph
124
+ * forbids, on the happy path, while an operator making the same move by hand
125
+ * was refused. Existence is now the port's `unknown_status` answer and
126
+ * reachability its `not_permitted` one, and both leave the order where it is.
127
+ */
128
+ export declare class ReceivePaymentHandler {
129
+ private readonly emFactory;
130
+ /**
131
+ * The method behind the payment, as `payment_methods` publishes it. Read
132
+ * inside the settlement transaction, as `shipments` reads its delivery
133
+ * method: it is a read of a row this transaction never writes, so it is not
134
+ * held by `payments_order_fk` and carries none of the atomicity the `Order`
135
+ * import above does. When `payment_methods` is off it throws, inside the
136
+ * transaction, and the payment and the order roll back together.
137
+ */
138
+ private readonly paymentMethodRead;
139
+ /**
140
+ * The lifecycle write (feature 085 Phase D). It validates against the
141
+ * configured graph, runs the veto guards, records the audit entry and
142
+ * applies the status's side-effects — the five things the assignment this
143
+ * replaced did none of, which is why a payment-driven cancellation used to
144
+ * leave the order's stock allocated forever and audited nowhere.
145
+ *
146
+ * **Called after this handler's transaction has committed**, never inside
147
+ * it: the port takes its own `EntityManager`, so a call from inside would
148
+ * write the order on a different pooled connection whose commit the
149
+ * enclosing rollback cannot reach (issue #200). The port's own doc block
150
+ * states the obligation.
151
+ *
152
+ * It announces the change too — `OrderTransitionService` emits the
153
+ * templated `order.status.*.after` events itself — so this handler no
154
+ * longer resolves `orderStatusAnnouncePort`. Doing both would emit every
155
+ * payment-driven status change twice.
156
+ */
157
+ private readonly orderTransition;
158
+ /**
159
+ * The money axis of the order this payment settles (feature 080, T048).
160
+ *
161
+ * **Called inside this handler's transaction**, unlike `orderTransition`
162
+ * above, and the asymmetry is the whole reason there are two ports rather
163
+ * than one: `payments_order_fk` (`on delete restrict`) holds the payment row
164
+ * and `orders.payment_status` together, so that write may not commit
165
+ * separately from this one — while a *lifecycle* transition obtains its own
166
+ * `EntityManager` and may not run inside it. The interface takes the
167
+ * caller's `EntityManager` as a required parameter for exactly that reason
168
+ * (D-169), and the header above states the constraint in full.
169
+ */
170
+ private readonly orderPaymentStatus;
171
+ /** Where a refused transition is recorded; see {@link SettlementLogger}. */
172
+ private readonly log;
173
+ private readonly events?;
174
+ constructor(emFactory: () => EntityManager,
175
+ /**
176
+ * The method behind the payment, as `payment_methods` publishes it. Read
177
+ * inside the settlement transaction, as `shipments` reads its delivery
178
+ * method: it is a read of a row this transaction never writes, so it is not
179
+ * held by `payments_order_fk` and carries none of the atomicity the `Order`
180
+ * import above does. When `payment_methods` is off it throws, inside the
181
+ * transaction, and the payment and the order roll back together.
182
+ */
183
+ paymentMethodRead: PaymentMethodReadPort,
184
+ /**
185
+ * The lifecycle write (feature 085 Phase D). It validates against the
186
+ * configured graph, runs the veto guards, records the audit entry and
187
+ * applies the status's side-effects — the five things the assignment this
188
+ * replaced did none of, which is why a payment-driven cancellation used to
189
+ * leave the order's stock allocated forever and audited nowhere.
190
+ *
191
+ * **Called after this handler's transaction has committed**, never inside
192
+ * it: the port takes its own `EntityManager`, so a call from inside would
193
+ * write the order on a different pooled connection whose commit the
194
+ * enclosing rollback cannot reach (issue #200). The port's own doc block
195
+ * states the obligation.
196
+ *
197
+ * It announces the change too — `OrderTransitionService` emits the
198
+ * templated `order.status.*.after` events itself — so this handler no
199
+ * longer resolves `orderStatusAnnouncePort`. Doing both would emit every
200
+ * payment-driven status change twice.
201
+ */
202
+ orderTransition: OrderTransitionPort,
203
+ /**
204
+ * The money axis of the order this payment settles (feature 080, T048).
205
+ *
206
+ * **Called inside this handler's transaction**, unlike `orderTransition`
207
+ * above, and the asymmetry is the whole reason there are two ports rather
208
+ * than one: `payments_order_fk` (`on delete restrict`) holds the payment row
209
+ * and `orders.payment_status` together, so that write may not commit
210
+ * separately from this one — while a *lifecycle* transition obtains its own
211
+ * `EntityManager` and may not run inside it. The interface takes the
212
+ * caller's `EntityManager` as a required parameter for exactly that reason
213
+ * (D-169), and the header above states the constraint in full.
214
+ */
215
+ orderPaymentStatus: OrderPaymentStatusApplyPort,
216
+ /** Where a refused transition is recorded; see {@link SettlementLogger}. */
217
+ log: SettlementLogger, events?: PaymentEventBus | undefined);
218
+ receive(input: ReceivePayment): Promise<ReceivePaymentResult>;
219
+ /**
220
+ * Ask the lifecycle for the move the configured setting names, and answer
221
+ * where the order ended up.
222
+ *
223
+ * Every refusal is a 200 for the provider (contract §"What the caller does
224
+ * with each outcome"): a PSP retries a non-2xx callback indefinitely, and an
225
+ * order an operator has already advanced past payment must not be dragged
226
+ * backwards because a delayed webhook arrived. So the refusal is recorded
227
+ * here and the settlement stands.
228
+ *
229
+ * There is deliberately no `catch`. A `ModuleDisabledError` out of the port's
230
+ * gate is not a refusal — it is the owner being absent — and swallowing it
231
+ * would turn fail-closed into fail-open.
232
+ */
233
+ private moveOrder;
234
+ /**
235
+ * Reflect a gateway refund onto the Payment + Order (feature 049). Driven by
236
+ * the `charge.refunded` webhook for BOTH Dashboard- and platform-initiated
237
+ * refunds. `refundedAmount` is the gateway's cumulative total, so this is
238
+ * idempotent by construction (re-applying the same total is a no-op) and never
239
+ * calls the gateway back (no refund loop). A fully-refunded payment is never
240
+ * downgraded to partial.
241
+ */
242
+ reflectRefund(input: {
243
+ paymentId?: string;
244
+ orderId?: string;
245
+ /** PaymentIntent id (`pi_…`) — resolves the payment when no paymentId. */
246
+ externalReference?: string;
247
+ /** Cumulative refunded amount in major units. */
248
+ refundedAmount: number;
249
+ currency: string;
250
+ fullyRefunded: boolean;
251
+ externalRefundId?: string | null;
252
+ providerDetails?: Record<string, unknown>;
253
+ }): Promise<{
254
+ paymentId: string;
255
+ changed: boolean;
256
+ } | null>;
257
+ private resolvePaymentForRefund;
258
+ private resolvePayment;
259
+ }
260
+ //# sourceMappingURL=receive-payment-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receive-payment-handler.d.ts","sourceRoot":"","sources":["../../../src/backend/services/receive-payment-handler.ts"],"names":[],"mappings":"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"}