@mj-biz-apps/orders-core-entities-server 0.0.1 → 5.2.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 (267) hide show
  1. package/LICENSE +183 -0
  2. package/README.md +141 -2
  3. package/dist/AccountingBridge.d.ts +65 -0
  4. package/dist/AccountingBridge.d.ts.map +1 -0
  5. package/dist/AccountingBridge.js +102 -0
  6. package/dist/AccountingBridge.js.map +1 -0
  7. package/dist/AdvanceOrderStateOperation.d.ts +80 -0
  8. package/dist/AdvanceOrderStateOperation.d.ts.map +1 -0
  9. package/dist/AdvanceOrderStateOperation.js +267 -0
  10. package/dist/AdvanceOrderStateOperation.js.map +1 -0
  11. package/dist/ApplyAccountCreditOperation.d.ts +97 -0
  12. package/dist/ApplyAccountCreditOperation.d.ts.map +1 -0
  13. package/dist/ApplyAccountCreditOperation.js +275 -0
  14. package/dist/ApplyAccountCreditOperation.js.map +1 -0
  15. package/dist/BaseDeliveryChannel.d.ts +106 -0
  16. package/dist/BaseDeliveryChannel.d.ts.map +1 -0
  17. package/dist/BaseDeliveryChannel.js +36 -0
  18. package/dist/BaseDeliveryChannel.js.map +1 -0
  19. package/dist/BasePaymentProvider.d.ts +239 -0
  20. package/dist/BasePaymentProvider.d.ts.map +1 -0
  21. package/dist/BasePaymentProvider.js +91 -0
  22. package/dist/BasePaymentProvider.js.map +1 -0
  23. package/dist/BundleBehavior.d.ts +139 -0
  24. package/dist/BundleBehavior.d.ts.map +1 -0
  25. package/dist/BundleBehavior.js +173 -0
  26. package/dist/BundleBehavior.js.map +1 -0
  27. package/dist/BundleEngine.d.ts +60 -0
  28. package/dist/BundleEngine.d.ts.map +1 -0
  29. package/dist/BundleEngine.js +197 -0
  30. package/dist/BundleEngine.js.map +1 -0
  31. package/dist/CancelSubscriptionOperation.d.ts +98 -0
  32. package/dist/CancelSubscriptionOperation.d.ts.map +1 -0
  33. package/dist/CancelSubscriptionOperation.js +317 -0
  34. package/dist/CancelSubscriptionOperation.js.map +1 -0
  35. package/dist/CapturePaymentOperation.d.ts +62 -0
  36. package/dist/CapturePaymentOperation.d.ts.map +1 -0
  37. package/dist/CapturePaymentOperation.js +463 -0
  38. package/dist/CapturePaymentOperation.js.map +1 -0
  39. package/dist/CheckEntitlementOperation.d.ts +27 -0
  40. package/dist/CheckEntitlementOperation.d.ts.map +1 -0
  41. package/dist/CheckEntitlementOperation.js +45 -0
  42. package/dist/CheckEntitlementOperation.js.map +1 -0
  43. package/dist/CheckoutSessionService.d.ts +258 -0
  44. package/dist/CheckoutSessionService.d.ts.map +1 -0
  45. package/dist/CheckoutSessionService.js +1557 -0
  46. package/dist/CheckoutSessionService.js.map +1 -0
  47. package/dist/DeliveryBehavior.d.ts +121 -0
  48. package/dist/DeliveryBehavior.d.ts.map +1 -0
  49. package/dist/DeliveryBehavior.js +145 -0
  50. package/dist/DeliveryBehavior.js.map +1 -0
  51. package/dist/DeliveryRecipientResolver.d.ts +38 -0
  52. package/dist/DeliveryRecipientResolver.d.ts.map +1 -0
  53. package/dist/DeliveryRecipientResolver.js +93 -0
  54. package/dist/DeliveryRecipientResolver.js.map +1 -0
  55. package/dist/DeliveryResolver.d.ts +14 -0
  56. package/dist/DeliveryResolver.d.ts.map +1 -0
  57. package/dist/DeliveryResolver.js +45 -0
  58. package/dist/DeliveryResolver.js.map +1 -0
  59. package/dist/EmailDeliveryChannel.d.ts +31 -0
  60. package/dist/EmailDeliveryChannel.d.ts.map +1 -0
  61. package/dist/EmailDeliveryChannel.js +158 -0
  62. package/dist/EmailDeliveryChannel.js.map +1 -0
  63. package/dist/EntitlementBehavior.d.ts +235 -0
  64. package/dist/EntitlementBehavior.d.ts.map +1 -0
  65. package/dist/EntitlementBehavior.js +330 -0
  66. package/dist/EntitlementBehavior.js.map +1 -0
  67. package/dist/EntitlementEngine.d.ts +97 -0
  68. package/dist/EntitlementEngine.d.ts.map +1 -0
  69. package/dist/EntitlementEngine.js +338 -0
  70. package/dist/EntitlementEngine.js.map +1 -0
  71. package/dist/EntitlementGrantClaimDriver.d.ts +42 -0
  72. package/dist/EntitlementGrantClaimDriver.d.ts.map +1 -0
  73. package/dist/EntitlementGrantClaimDriver.js +158 -0
  74. package/dist/EntitlementGrantClaimDriver.js.map +1 -0
  75. package/dist/EntitlementRead.d.ts +82 -0
  76. package/dist/EntitlementRead.d.ts.map +1 -0
  77. package/dist/EntitlementRead.js +368 -0
  78. package/dist/EntitlementRead.js.map +1 -0
  79. package/dist/FulfillOrderLinesOperation.d.ts +34 -0
  80. package/dist/FulfillOrderLinesOperation.d.ts.map +1 -0
  81. package/dist/FulfillOrderLinesOperation.js +208 -0
  82. package/dist/FulfillOrderLinesOperation.js.map +1 -0
  83. package/dist/FulfillmentBehavior.d.ts +101 -0
  84. package/dist/FulfillmentBehavior.d.ts.map +1 -0
  85. package/dist/FulfillmentBehavior.js +145 -0
  86. package/dist/FulfillmentBehavior.js.map +1 -0
  87. package/dist/GLAccountResolver.d.ts +123 -0
  88. package/dist/GLAccountResolver.d.ts.map +1 -0
  89. package/dist/GLAccountResolver.js +175 -0
  90. package/dist/GLAccountResolver.js.map +1 -0
  91. package/dist/GetFulfillmentQueueOperation.d.ts +35 -0
  92. package/dist/GetFulfillmentQueueOperation.d.ts.map +1 -0
  93. package/dist/GetFulfillmentQueueOperation.js +221 -0
  94. package/dist/GetFulfillmentQueueOperation.js.map +1 -0
  95. package/dist/GetOverdueWorklistOperation.d.ts +41 -0
  96. package/dist/GetOverdueWorklistOperation.d.ts.map +1 -0
  97. package/dist/GetOverdueWorklistOperation.js +210 -0
  98. package/dist/GetOverdueWorklistOperation.js.map +1 -0
  99. package/dist/GiftCardBehavior.d.ts +97 -0
  100. package/dist/GiftCardBehavior.d.ts.map +1 -0
  101. package/dist/GiftCardBehavior.js +121 -0
  102. package/dist/GiftCardBehavior.js.map +1 -0
  103. package/dist/GiftCardEngine.d.ts +59 -0
  104. package/dist/GiftCardEngine.d.ts.map +1 -0
  105. package/dist/GiftCardEngine.js +197 -0
  106. package/dist/GiftCardEngine.js.map +1 -0
  107. package/dist/GuestOrderClaimDriver.d.ts +36 -0
  108. package/dist/GuestOrderClaimDriver.d.ts.map +1 -0
  109. package/dist/GuestOrderClaimDriver.js +161 -0
  110. package/dist/GuestOrderClaimDriver.js.map +1 -0
  111. package/dist/InvoiceBehavior.d.ts +394 -0
  112. package/dist/InvoiceBehavior.d.ts.map +1 -0
  113. package/dist/InvoiceBehavior.js +496 -0
  114. package/dist/InvoiceBehavior.js.map +1 -0
  115. package/dist/InvoiceBuilder.d.ts +48 -0
  116. package/dist/InvoiceBuilder.d.ts.map +1 -0
  117. package/dist/InvoiceBuilder.js +352 -0
  118. package/dist/InvoiceBuilder.js.map +1 -0
  119. package/dist/InvoiceDisplay.d.ts +96 -0
  120. package/dist/InvoiceDisplay.d.ts.map +1 -0
  121. package/dist/InvoiceDisplay.js +121 -0
  122. package/dist/InvoiceDisplay.js.map +1 -0
  123. package/dist/ListEntitlementsOperation.d.ts +21 -0
  124. package/dist/ListEntitlementsOperation.d.ts.map +1 -0
  125. package/dist/ListEntitlementsOperation.js +39 -0
  126. package/dist/ListEntitlementsOperation.js.map +1 -0
  127. package/dist/ManualPaymentProvider.d.ts +19 -0
  128. package/dist/ManualPaymentProvider.d.ts.map +1 -0
  129. package/dist/ManualPaymentProvider.js +92 -0
  130. package/dist/ManualPaymentProvider.js.map +1 -0
  131. package/dist/OrderEntityServer.d.ts +542 -0
  132. package/dist/OrderEntityServer.d.ts.map +1 -0
  133. package/dist/OrderEntityServer.js +2100 -0
  134. package/dist/OrderEntityServer.js.map +1 -0
  135. package/dist/OrderJournalEntryFactory.d.ts +140 -0
  136. package/dist/OrderJournalEntryFactory.d.ts.map +1 -0
  137. package/dist/OrderJournalEntryFactory.js +466 -0
  138. package/dist/OrderJournalEntryFactory.js.map +1 -0
  139. package/dist/OrderLineEntityServer.d.ts +90 -0
  140. package/dist/OrderLineEntityServer.d.ts.map +1 -0
  141. package/dist/OrderLineEntityServer.js +260 -0
  142. package/dist/OrderLineEntityServer.js.map +1 -0
  143. package/dist/OrdersSettings.d.ts +36 -0
  144. package/dist/OrdersSettings.d.ts.map +1 -0
  145. package/dist/OrdersSettings.js +147 -0
  146. package/dist/OrdersSettings.js.map +1 -0
  147. package/dist/PaymentAllocationFactory.d.ts +128 -0
  148. package/dist/PaymentAllocationFactory.d.ts.map +1 -0
  149. package/dist/PaymentAllocationFactory.js +235 -0
  150. package/dist/PaymentAllocationFactory.js.map +1 -0
  151. package/dist/PaymentHeaderEntityServer.d.ts +180 -0
  152. package/dist/PaymentHeaderEntityServer.d.ts.map +1 -0
  153. package/dist/PaymentHeaderEntityServer.js +656 -0
  154. package/dist/PaymentHeaderEntityServer.js.map +1 -0
  155. package/dist/PaymentIntentService.d.ts +101 -0
  156. package/dist/PaymentIntentService.d.ts.map +1 -0
  157. package/dist/PaymentIntentService.js +150 -0
  158. package/dist/PaymentIntentService.js.map +1 -0
  159. package/dist/PaymentJournalEntryFactory.d.ts +99 -0
  160. package/dist/PaymentJournalEntryFactory.d.ts.map +1 -0
  161. package/dist/PaymentJournalEntryFactory.js +127 -0
  162. package/dist/PaymentJournalEntryFactory.js.map +1 -0
  163. package/dist/PaymentLineEntityServer.d.ts +73 -0
  164. package/dist/PaymentLineEntityServer.d.ts.map +1 -0
  165. package/dist/PaymentLineEntityServer.js +303 -0
  166. package/dist/PaymentLineEntityServer.js.map +1 -0
  167. package/dist/PaymentProviderBehavior.d.ts +222 -0
  168. package/dist/PaymentProviderBehavior.d.ts.map +1 -0
  169. package/dist/PaymentProviderBehavior.js +368 -0
  170. package/dist/PaymentProviderBehavior.js.map +1 -0
  171. package/dist/PaymentProviderResolver.d.ts +84 -0
  172. package/dist/PaymentProviderResolver.d.ts.map +1 -0
  173. package/dist/PaymentProviderResolver.js +220 -0
  174. package/dist/PaymentProviderResolver.js.map +1 -0
  175. package/dist/PaymentReversalFactory.d.ts +107 -0
  176. package/dist/PaymentReversalFactory.d.ts.map +1 -0
  177. package/dist/PaymentReversalFactory.js +174 -0
  178. package/dist/PaymentReversalFactory.js.map +1 -0
  179. package/dist/PaymentSettlement.d.ts +56 -0
  180. package/dist/PaymentSettlement.d.ts.map +1 -0
  181. package/dist/PaymentSettlement.js +243 -0
  182. package/dist/PaymentSettlement.js.map +1 -0
  183. package/dist/PaymentTermsBehavior.d.ts +109 -0
  184. package/dist/PaymentTermsBehavior.d.ts.map +1 -0
  185. package/dist/PaymentTermsBehavior.js +172 -0
  186. package/dist/PaymentTermsBehavior.js.map +1 -0
  187. package/dist/PaymentWebhookHandler.d.ts +103 -0
  188. package/dist/PaymentWebhookHandler.d.ts.map +1 -0
  189. package/dist/PaymentWebhookHandler.js +246 -0
  190. package/dist/PaymentWebhookHandler.js.map +1 -0
  191. package/dist/PreviewPriceOperation.d.ts +62 -0
  192. package/dist/PreviewPriceOperation.d.ts.map +1 -0
  193. package/dist/PreviewPriceOperation.js +161 -0
  194. package/dist/PreviewPriceOperation.js.map +1 -0
  195. package/dist/PriceOrderOperation.d.ts +93 -0
  196. package/dist/PriceOrderOperation.d.ts.map +1 -0
  197. package/dist/PriceOrderOperation.js +146 -0
  198. package/dist/PriceOrderOperation.js.map +1 -0
  199. package/dist/ProductPriceEntityServer.d.ts +34 -0
  200. package/dist/ProductPriceEntityServer.d.ts.map +1 -0
  201. package/dist/ProductPriceEntityServer.js +97 -0
  202. package/dist/ProductPriceEntityServer.js.map +1 -0
  203. package/dist/RefundPaymentOperation.d.ts +61 -0
  204. package/dist/RefundPaymentOperation.d.ts.map +1 -0
  205. package/dist/RefundPaymentOperation.js +177 -0
  206. package/dist/RefundPaymentOperation.js.map +1 -0
  207. package/dist/RevenueRecognition.d.ts +76 -0
  208. package/dist/RevenueRecognition.d.ts.map +1 -0
  209. package/dist/RevenueRecognition.js +133 -0
  210. package/dist/RevenueRecognition.js.map +1 -0
  211. package/dist/ReversalBehavior.d.ts +82 -0
  212. package/dist/ReversalBehavior.d.ts.map +1 -0
  213. package/dist/ReversalBehavior.js +100 -0
  214. package/dist/ReversalBehavior.js.map +1 -0
  215. package/dist/ReversalResolver.d.ts +37 -0
  216. package/dist/ReversalResolver.d.ts.map +1 -0
  217. package/dist/ReversalResolver.js +96 -0
  218. package/dist/ReversalResolver.js.map +1 -0
  219. package/dist/SpawnRenewalsOperation.d.ts +109 -0
  220. package/dist/SpawnRenewalsOperation.d.ts.map +1 -0
  221. package/dist/SpawnRenewalsOperation.js +295 -0
  222. package/dist/SpawnRenewalsOperation.js.map +1 -0
  223. package/dist/StoredValuePaymentProvider.d.ts +52 -0
  224. package/dist/StoredValuePaymentProvider.d.ts.map +1 -0
  225. package/dist/StoredValuePaymentProvider.js +205 -0
  226. package/dist/StoredValuePaymentProvider.js.map +1 -0
  227. package/dist/StripeACHPaymentProvider.d.ts +50 -0
  228. package/dist/StripeACHPaymentProvider.d.ts.map +1 -0
  229. package/dist/StripeACHPaymentProvider.js +211 -0
  230. package/dist/StripeACHPaymentProvider.js.map +1 -0
  231. package/dist/StripePaymentProvider.d.ts +83 -0
  232. package/dist/StripePaymentProvider.d.ts.map +1 -0
  233. package/dist/StripePaymentProvider.js +442 -0
  234. package/dist/StripePaymentProvider.js.map +1 -0
  235. package/dist/SubscriptionBehavior.d.ts +197 -0
  236. package/dist/SubscriptionBehavior.d.ts.map +1 -0
  237. package/dist/SubscriptionBehavior.js +415 -0
  238. package/dist/SubscriptionBehavior.js.map +1 -0
  239. package/dist/checkoutCaptureAlert.d.ts +16 -0
  240. package/dist/checkoutCaptureAlert.d.ts.map +1 -0
  241. package/dist/checkoutCaptureAlert.js +55 -0
  242. package/dist/checkoutCaptureAlert.js.map +1 -0
  243. package/dist/checkoutCaptureRetry.d.ts +27 -0
  244. package/dist/checkoutCaptureRetry.d.ts.map +1 -0
  245. package/dist/checkoutCaptureRetry.js +50 -0
  246. package/dist/checkoutCaptureRetry.js.map +1 -0
  247. package/dist/claimDriverHelpers.d.ts +15 -0
  248. package/dist/claimDriverHelpers.d.ts.map +1 -0
  249. package/dist/claimDriverHelpers.js +34 -0
  250. package/dist/claimDriverHelpers.js.map +1 -0
  251. package/dist/entity-names.d.ts +15 -0
  252. package/dist/entity-names.d.ts.map +1 -0
  253. package/dist/entity-names.js +15 -0
  254. package/dist/entity-names.js.map +1 -0
  255. package/dist/identityClaimContracts.d.ts +118 -0
  256. package/dist/identityClaimContracts.d.ts.map +1 -0
  257. package/dist/identityClaimContracts.js +58 -0
  258. package/dist/identityClaimContracts.js.map +1 -0
  259. package/dist/index.d.ts +126 -0
  260. package/dist/index.d.ts.map +1 -0
  261. package/dist/index.js +115 -0
  262. package/dist/index.js.map +1 -0
  263. package/dist/sql-guards.d.ts +78 -0
  264. package/dist/sql-guards.d.ts.map +1 -0
  265. package/dist/sql-guards.js +115 -0
  266. package/dist/sql-guards.js.map +1 -0
  267. package/package.json +50 -5
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Promoting a bank debit into cash — the only place a webhook is allowed to move money.
3
+ *
4
+ * WHY THIS EXISTS AT ALL. `PaymentWebhookHandler` was written deliberately narrow: it records what the
5
+ * gateway said on the `PaymentIntent` and stops there, because a webhook reaching into the ledger
6
+ * would give an unauthenticated endpoint a second path into the books. That was exactly right for
7
+ * cards, where the money has already moved by the time anyone asks and the event only confirms it.
8
+ *
9
+ * A bank debit breaks the assumption underneath it. Nothing has moved when the caller asks; the answer
10
+ * arrives days later, in an event, and there is no other moment at which cash can honestly be booked.
11
+ * So the webhook has to be able to move money — and this module is that capability, kept in one file,
12
+ * reachable only when a driver has declared `SettlesAsynchronously`, and doing exactly three things.
13
+ *
14
+ * IT DOES NOT BOOK ANYTHING ITSELF. That is the part worth being clear about. `Promote` sets a status
15
+ * and saves; `PaymentHeaderEntityServer` notices the transition into `Captured` and books the cash
16
+ * through the same path a card capture uses — including calling back to the driver to ask what
17
+ * actually moved. `Reverse` writes a reversing payment through the same factory `Orders.RefundPayment`
18
+ * uses. Nothing here writes a journal entry, and nothing here knows how one is shaped. The ledger keeps
19
+ * a single author.
20
+ *
21
+ * ORDERING MATTERS, AND IT IS NOT THE OBVIOUS ONE. This runs BEFORE the handler stamps
22
+ * `PaymentIntent.ProviderEventID`, not after. Stamping first would look tidier and would silently
23
+ * strand payments: the stamp is the idempotency key, so an event that was recorded and then failed to
24
+ * settle would be judged `AlreadyApplied` on every retry, and a payment the bank confirmed would sit
25
+ * `Pending` forever with nothing left to move it. Settling first means a failure returns 500 with
26
+ * nothing stamped, the gateway retries, and both steps run again. The re-run is safe because
27
+ * `DecideSettlement` reads the PAYMENT'S state rather than the event's novelty — a promotion that
28
+ * already happened decides `None`.
29
+ *
30
+ * CONNECTS TO:
31
+ * PURE: ./PaymentProviderBehavior.ts — DecideSettlement, the decision table
32
+ * ROUTE: ./PaymentWebhookHandler.ts — the only caller
33
+ * WRITES: ./PaymentHeaderEntityServer.ts (promote) · ./PaymentReversalFactory.ts (reverse)
34
+ * DOC: plans/archive/bizapps-orders-master.md D17, D19, D53
35
+ */
36
+ import { CompositeKey, LogError, LogStatus, RunView, } from '@memberjunction/core';
37
+ import { DecideSettlement } from './PaymentProviderBehavior.js';
38
+ import { BuildUnapplyLines, CreateReversingPayment, LoadAppliedAllocations, } from './PaymentReversalFactory.js';
39
+ const PAYMENT_HEADER_ENTITY = 'MJ_BizApps_Orders: Payment Headers';
40
+ const PAYMENT_LINE_ENTITY = 'MJ_BizApps_Orders: Payment Lines';
41
+ /**
42
+ * Apply a settlement event to the payment behind its intent.
43
+ *
44
+ * THROWS ON FAULTS, DELIBERATELY. A refusal here is not a business outcome to be reported and
45
+ * forgotten — it means the bank told us something about real money and we failed to record it. The
46
+ * caller turns a throw into a 500 so the gateway asks again, which is the only way that notice is not
47
+ * lost. Decisions that legitimately do nothing (`None`, `Hold`) return normally.
48
+ */
49
+ export async function SettlePaymentForEvent(event, paymentIntentID, provider, user) {
50
+ const payment = await loadPaymentForIntent(provider, user, paymentIntentID);
51
+ const alreadyReversed = payment ? await hasReversal(provider, user, payment.ID) : false;
52
+ // WHETHER THIS PAYMENT PUT CASH IN THE LEDGER IS A QUESTION ABOUT ITS ALLOCATIONS, NOT ITS HEADER.
53
+ //
54
+ // This read used to be `Boolean(payment.JournalEntryID)`, and that stopped being the same question
55
+ // twice over. The cash leg moved to the allocation (D13), so the header's journal entry is only
56
+ // ever the PROCESSING FEE — and D82 turned the fee entry off for every tender by default. Between
57
+ // them, `JournalEntryID` is now null on essentially every payment that ever books.
58
+ //
59
+ // The decision table reads `HeaderBooked` to choose between reversing a returned debit and holding
60
+ // it for a person ("Captured but carries no journal entry, so what to reverse is unclear"). With
61
+ // the old signal every returned ACH debit took the Hold branch: the cash stayed in the ledger, the
62
+ // order stayed marked paid, and the only outward sign was a settlement waiting on a human who was
63
+ // never told. Asking the allocations restores the question the table was written to ask.
64
+ const booked = payment ? await hasBookedAllocations(provider, user, payment.ID) : false;
65
+ const decision = DecideSettlement({
66
+ EventStatus: event.Status,
67
+ HeaderStatus: payment?.Status,
68
+ HeaderBooked: booked,
69
+ AlreadyReversed: alreadyReversed,
70
+ });
71
+ const base = {
72
+ Action: decision.Action,
73
+ Reason: decision.Reason,
74
+ PaymentHeaderID: payment?.ID,
75
+ };
76
+ switch (decision.Action) {
77
+ case 'Promote':
78
+ await promote(provider, user, payment, event);
79
+ LogStatus(`Payment ${payment.PaymentNumber} captured from bank-debit event ${event.EventID}.`);
80
+ return base;
81
+ case 'Fail':
82
+ await markFailed(provider, user, payment, event);
83
+ LogStatus(`Payment ${payment.PaymentNumber} failed from bank-debit event ${event.EventID}` +
84
+ `${event.FailureReason ? ` (${event.FailureReason})` : ''}.`);
85
+ return base;
86
+ case 'Reverse': {
87
+ const reversalID = await reverse(provider, user, payment, event);
88
+ LogStatus(`Payment ${payment.PaymentNumber} was returned by the bank and has been reversed ` +
89
+ `from event ${event.EventID}.`);
90
+ return { ...base, ReversalPaymentHeaderID: reversalID };
91
+ }
92
+ case 'Hold':
93
+ // LOUD, because nothing else will be. A held event is money whose state we have declined to
94
+ // guess at, and the only thing standing between that and a silent discrepancy is this line.
95
+ LogError(`Bank-debit event ${event.EventID} was NOT applied to payment ` +
96
+ `${payment?.PaymentNumber ?? '(none)'}: ${decision.Reason}. This needs a person.`);
97
+ return base;
98
+ default:
99
+ LogStatus(`Bank-debit event ${event.EventID}: ${decision.Reason}`);
100
+ return base;
101
+ }
102
+ }
103
+ // ─── Reads ─────────────────────────────────────────────────────────────────────────────────────
104
+ async function loadPaymentForIntent(provider, user, paymentIntentID) {
105
+ const rv = new RunView(provider);
106
+ const result = await rv.RunView({
107
+ EntityName: PAYMENT_HEADER_ENTITY,
108
+ ExtraFilter: `PaymentIntentID='${paymentIntentID}'`,
109
+ Fields: [
110
+ 'ID',
111
+ 'PaymentNumber',
112
+ 'ReceivingCompanyID',
113
+ 'BillToOrganizationID',
114
+ 'BillToPersonID',
115
+ 'PaymentTypeID',
116
+ 'PaymentDetailID',
117
+ 'Amount',
118
+ 'Status',
119
+ 'JournalEntryID',
120
+ ],
121
+ ResultType: 'simple',
122
+ // The status is the whole decision, and it may have been written by the previous delivery
123
+ // of a related event moments ago. A cached read here re-promotes an already-captured
124
+ // payment or reverses one twice.
125
+ BypassCache: true,
126
+ }, user);
127
+ return result?.Results?.[0] ?? null;
128
+ }
129
+ /**
130
+ * True when any of this payment's allocations has booked — i.e. the cash is in the ledger.
131
+ *
132
+ * `PaymentLine.BookedAt` is the allocation's own idempotency key, set in the same transaction that
133
+ * writes its journal entry, so it is the authority on whether the money exists. Reading one booked
134
+ * row is enough: allocations book together with the capture.
135
+ *
136
+ * `BypassCache` for the same reason the header read has it — the promotion that booked these lines
137
+ * may have happened moments ago, on a previous delivery of a related event.
138
+ */
139
+ async function hasBookedAllocations(provider, user, paymentID) {
140
+ const rv = new RunView(provider);
141
+ const result = await rv.RunView({
142
+ EntityName: PAYMENT_LINE_ENTITY,
143
+ ExtraFilter: `PaymentHeaderID='${paymentID}' AND BookedAt IS NOT NULL`,
144
+ Fields: ['ID'],
145
+ MaxRows: 1,
146
+ ResultType: 'simple',
147
+ BypassCache: true,
148
+ }, user);
149
+ return (result?.Results?.length ?? 0) > 0;
150
+ }
151
+ /** True when a reversing payment already points at this one. */
152
+ async function hasReversal(provider, user, paymentID) {
153
+ const rv = new RunView(provider);
154
+ const result = await rv.RunView({
155
+ EntityName: PAYMENT_HEADER_ENTITY,
156
+ ExtraFilter: `ReversesPaymentHeaderID='${paymentID}' AND Status='Refunded'`,
157
+ Fields: ['ID'],
158
+ ResultType: 'simple',
159
+ BypassCache: true,
160
+ }, user);
161
+ return (result?.Results?.length ?? 0) > 0;
162
+ }
163
+ // ─── Writes ────────────────────────────────────────────────────────────────────────────────────
164
+ /**
165
+ * Pending → Captured, which is what books the cash.
166
+ *
167
+ * NOTHING IS SET BUT THE STATUS. `PaymentHeaderEntityServer` calls the driver on this transition to
168
+ * ask what actually moved, and overwrites `Amount`, `ProcessingFeeAmount` and `NetAmount` from the
169
+ * answer. Setting them from the webhook payload here would put our reading of the event in a race with
170
+ * the gateway's own record of the charge — and the gateway wins that argument every time, so writing
171
+ * ours would be work whose only possible outcome is being overwritten or being wrong.
172
+ */
173
+ async function promote(provider, user, payment, event) {
174
+ const header = await provider.GetEntityObject(PAYMENT_HEADER_ENTITY, CompositeKey.FromID(payment.ID), user);
175
+ header.Status = 'Captured';
176
+ if (!(await header.Save())) {
177
+ throw new Error(`Could not capture payment ${payment.PaymentNumber} from event ${event.EventID}: ` +
178
+ `${header.LatestResult?.CompleteMessage ?? 'unknown error'}`);
179
+ }
180
+ }
181
+ /**
182
+ * Pending → Failed. Nothing was booked, so nothing needs reversing.
183
+ *
184
+ * The bank's own words go on `Notes` rather than `ReversalReason` — that column belongs to reversals,
185
+ * and a failed payment did not reverse anything. Appending rather than replacing keeps whatever a
186
+ * person had already written there.
187
+ */
188
+ async function markFailed(provider, user, payment, event) {
189
+ const header = await provider.GetEntityObject(PAYMENT_HEADER_ENTITY, CompositeKey.FromID(payment.ID), user);
190
+ header.Status = 'Failed';
191
+ if (event.FailureReason) {
192
+ const existing = header.Notes ?? '';
193
+ const note = `Bank debit did not clear: ${event.FailureReason}`;
194
+ header.Notes = existing ? `${existing}\n${note}` : note;
195
+ }
196
+ if (!(await header.Save())) {
197
+ throw new Error(`Could not mark payment ${payment.PaymentNumber} as failed from event ${event.EventID}: ` +
198
+ `${header.LatestResult?.CompleteMessage ?? 'unknown error'}`);
199
+ }
200
+ }
201
+ /**
202
+ * Captured → reversed by a new payment, because the bank took the money back.
203
+ *
204
+ * THE FULL AMOUNT, ALWAYS. A return is not a partial refund — the bank does not return part of a
205
+ * debit — so the reversal mirrors what was booked rather than anything on the event. Reading the
206
+ * amount off the event would be worse than useless here: a `charge.failed` payload reports the
207
+ * charge's amount, which is the same number when all is well and a silent under-reversal when it is
208
+ * not.
209
+ */
210
+ async function reverse(provider, user, payment, event) {
211
+ const applications = await LoadAppliedAllocations(provider, user, payment.ID);
212
+ if (!applications.length) {
213
+ throw new Error(`Payment ${payment.PaymentNumber} was returned by the bank but is not applied to any order, ` +
214
+ `so there is nothing to un-apply. This should be impossible for a captured payment.`);
215
+ }
216
+ const amount = Math.round(Math.abs(Number(payment.Amount ?? 0)) * 100) / 100;
217
+ const reason = event.FailureReason
218
+ ? `Bank debit returned: ${event.FailureReason}`
219
+ : 'Bank debit returned by the customer’s bank';
220
+ const dbProvider = provider;
221
+ await dbProvider.BeginTransaction();
222
+ try {
223
+ const lines = await BuildUnapplyLines(provider, user, applications, amount);
224
+ const reversal = await CreateReversingPayment(provider, user, payment, {
225
+ Amount: amount,
226
+ Reason: reason,
227
+ ProviderRefundID: event.ProviderChargeID ?? null,
228
+ Description: `Return of ${payment.PaymentNumber}`,
229
+ }, lines);
230
+ await dbProvider.CommitTransaction();
231
+ return reversal.ID;
232
+ }
233
+ catch (err) {
234
+ try {
235
+ await dbProvider.RollbackTransaction();
236
+ }
237
+ catch (rollbackErr) {
238
+ LogError(`Rollback failed after a bank-debit reversal error: ${rollbackErr}`);
239
+ }
240
+ throw err;
241
+ }
242
+ }
243
+ //# sourceMappingURL=PaymentSettlement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PaymentSettlement.js","sourceRoot":"","sources":["../src/PaymentSettlement.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @fileoverview When an order is due — the resolution walk, with no database (plan D83).
3
+ *
4
+ * WHY THIS EXISTS AT ALL. `OrderHeader.DueDate` was only ever what a caller passed, and nothing
5
+ * derived it from `PaymentTermsType.NetDays` even though the schema comment promised exactly that.
6
+ * `PaymentTermsType` had no rows, so nobody could pick terms either. The consequence was not a
7
+ * missing feature that looked missing:
8
+ *
9
+ * Orders.GetOverdueWorklist, as of 2026-12-31 → 0 rows, £0 overdue, every aging bucket zero
10
+ *
11
+ * against 67 orders carrying an unpaid balance. The collections surface reported a quiet afternoon
12
+ * because its only input was null on every row. Aging, the overdue worklist and the invoice's due
13
+ * date all read that one column.
14
+ *
15
+ * THE WALK, and it is the third of this shape in the app — GL account resolution (D5) and price
16
+ * resolution (D69) are the other two, so a reader already knows how to read it:
17
+ *
18
+ * 1. a STATED DueDate the caller knows the answer; never recomputed
19
+ * 2. a STATED PaymentTermsTypeID derive OrderDate + NetDays
20
+ * 3. the CUSTOMER's terms CustomerPaymentTerms, date-effective, optionally per company
21
+ * 4. the SELLING COMPANY's default AccountingCompanyProfile.DefaultPaymentTermsTypeID
22
+ * 5. due on receipt the terminal default
23
+ *
24
+ * WHERE CONTRACTS FIT. They do not — deliberately. A contracts app further down the graph populates
25
+ * the order's `DueDate` or `PaymentTermsTypeID` directly, and to Orders that is simply rung 1 or 2.
26
+ * Orders has no knowledge of contracts and should not grow any; "stated" is the whole interface.
27
+ *
28
+ * WHICH IS WHY STATED HAS TO BE RECORDED AS STATED. A due date that was supplied and one that was
29
+ * derived are the same value in the same column, and the difference only shows up on the next save —
30
+ * when a recompute silently moves a date somebody negotiated. `PricingBehavior` already draws this
31
+ * distinction for a stated unit price; this follows it.
32
+ *
33
+ * UNLIKE D5, IT DOES NOT FAIL LOUDLY. An unresolvable GL account means booked money with nowhere to
34
+ * go, so refusing is right. Missing terms have a sane answer — due on receipt — and refusing an
35
+ * order because nobody configured a lookup would be hostile for no gain.
36
+ *
37
+ * @module @mj-biz-apps/orders-core-entities-server
38
+ */
39
+ import { type DateCell } from '@mj-biz-apps/orders-entities';
40
+ /** A terms record as the walk needs it. */
41
+ export interface TermsFacts {
42
+ PaymentTermsTypeID: string;
43
+ /** Days from the order date to the due date. 0 means due on receipt. */
44
+ NetDays: number | null;
45
+ }
46
+ /** One rung 3 candidate: what a particular buyer negotiated. */
47
+ export interface CustomerTermsFacts extends TermsFacts {
48
+ /** Null when the terms apply whoever is selling. */
49
+ CompanyID: string | null;
50
+ StartedAt: DateCell;
51
+ EndedAt: DateCell;
52
+ Status: string;
53
+ }
54
+ /** Everything the walk may consult, gathered by the caller. */
55
+ export interface TermsResolutionInput {
56
+ /** What the caller stated on the order, if anything. */
57
+ StatedDueDate: DateCell;
58
+ StatedPaymentTermsTypeID: string | null;
59
+ /** The date the terms run from. */
60
+ OrderDate: string;
61
+ /** The selling company, used to narrow customer terms and to find the company default. */
62
+ CompanyID: string | null;
63
+ /** Rung 3 candidates for this buyer, in any order. */
64
+ CustomerTerms: readonly CustomerTermsFacts[];
65
+ /** Rung 4 — the selling company's default, or null when it has no profile. */
66
+ CompanyDefault: TermsFacts | null;
67
+ /** `NetDays` by terms id, for resolving a stated `PaymentTermsTypeID`. */
68
+ TermsByID: ReadonlyMap<string, TermsFacts>;
69
+ }
70
+ export type TermsSource = 'StatedDueDate' | 'StatedTerms' | 'CustomerTerms' | 'CompanyDefault' | 'DueOnReceipt';
71
+ export interface TermsResolution {
72
+ /** `YYYY-MM-DD`, or null only when the order date itself was unusable. */
73
+ DueDate: string | null;
74
+ /** The terms record the date came from, when one was involved. */
75
+ PaymentTermsTypeID: string | null;
76
+ /** Which rung answered — recorded so a later save can tell stated from derived. */
77
+ Source: TermsSource;
78
+ /** True when the caller supplied the date and it must never be recomputed. */
79
+ WasStated: boolean;
80
+ }
81
+ /** `from` plus `days`, as `YYYY-MM-DD`. */
82
+ export declare function AddDays(from: DateCell, days: number): string | null;
83
+ /**
84
+ * Whether a customer-terms row applies to this order.
85
+ *
86
+ * DATE-EFFECTIVE ON THE ORDER DATE, not on today. Renegotiating terms must not restate what an old
87
+ * order was due on — an order placed under Net 30 stays due when it was always due, however the
88
+ * relationship changed afterwards.
89
+ *
90
+ * A row scoped to a company applies only to that company's orders; an unscoped row applies to any.
91
+ */
92
+ export declare function CustomerTermsApply(row: CustomerTermsFacts, orderDate: DateCell, companyID: string | null): boolean;
93
+ /**
94
+ * The most specific applicable customer terms.
95
+ *
96
+ * COMPANY-SCOPED BEATS UNSCOPED, because a subsidiary that negotiated its own terms meant to
97
+ * override the group's. Among equally specific rows the one that started most recently wins — a
98
+ * later negotiation supersedes an earlier one — and a row with no start date is treated as having
99
+ * always applied, so it loses to any dated row.
100
+ */
101
+ export declare function BestCustomerTerms(rows: readonly CustomerTermsFacts[], orderDate: string, companyID: string | null): CustomerTermsFacts | null;
102
+ /**
103
+ * Walk the rungs and produce the due date.
104
+ *
105
+ * Never throws and never returns "unknown": every order gets a date or an explicit due-on-receipt,
106
+ * which is the order date itself.
107
+ */
108
+ export declare function ResolveDueDate(input: TermsResolutionInput): TermsResolution;
109
+ //# sourceMappingURL=PaymentTermsBehavior.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PaymentTermsBehavior.d.ts","sourceRoot":"","sources":["../src/PaymentTermsBehavior.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAExE,2CAA2C;AAC3C,MAAM,WAAW,UAAU;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,wEAAwE;IACxE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,gEAAgE;AAChE,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IAClD,oDAAoD;IACpD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,QAAQ,CAAC;IACpB,OAAO,EAAE,QAAQ,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,+DAA+D;AAC/D,MAAM,WAAW,oBAAoB;IACjC,wDAAwD;IACxD,aAAa,EAAE,QAAQ,CAAC;IACxB,wBAAwB,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,sDAAsD;IACtD,aAAa,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC7C,8EAA8E;IAC9E,cAAc,EAAE,UAAU,GAAG,IAAI,CAAC;IAClC,0EAA0E;IAC1E,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC9C;AAED,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,aAAa,GAAG,eAAe,GAAG,gBAAgB,GAAG,cAAc,CAAC;AAEhH,MAAM,WAAW,eAAe;IAC5B,0EAA0E;IAC1E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,kEAAkE;IAClE,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,mFAAmF;IACnF,MAAM,EAAE,WAAW,CAAC;IACpB,8EAA8E;IAC9E,SAAS,EAAE,OAAO,CAAC;CACtB;AAkBD,2CAA2C;AAC3C,wBAAgB,OAAO,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAInE;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,kBAAkB,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAYlH;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC7B,IAAI,EAAE,SAAS,kBAAkB,EAAE,EACnC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAAG,IAAI,GACzB,kBAAkB,GAAG,IAAI,CAa3B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,oBAAoB,GAAG,eAAe,CAwD3E"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @fileoverview When an order is due — the resolution walk, with no database (plan D83).
3
+ *
4
+ * WHY THIS EXISTS AT ALL. `OrderHeader.DueDate` was only ever what a caller passed, and nothing
5
+ * derived it from `PaymentTermsType.NetDays` even though the schema comment promised exactly that.
6
+ * `PaymentTermsType` had no rows, so nobody could pick terms either. The consequence was not a
7
+ * missing feature that looked missing:
8
+ *
9
+ * Orders.GetOverdueWorklist, as of 2026-12-31 → 0 rows, £0 overdue, every aging bucket zero
10
+ *
11
+ * against 67 orders carrying an unpaid balance. The collections surface reported a quiet afternoon
12
+ * because its only input was null on every row. Aging, the overdue worklist and the invoice's due
13
+ * date all read that one column.
14
+ *
15
+ * THE WALK, and it is the third of this shape in the app — GL account resolution (D5) and price
16
+ * resolution (D69) are the other two, so a reader already knows how to read it:
17
+ *
18
+ * 1. a STATED DueDate the caller knows the answer; never recomputed
19
+ * 2. a STATED PaymentTermsTypeID derive OrderDate + NetDays
20
+ * 3. the CUSTOMER's terms CustomerPaymentTerms, date-effective, optionally per company
21
+ * 4. the SELLING COMPANY's default AccountingCompanyProfile.DefaultPaymentTermsTypeID
22
+ * 5. due on receipt the terminal default
23
+ *
24
+ * WHERE CONTRACTS FIT. They do not — deliberately. A contracts app further down the graph populates
25
+ * the order's `DueDate` or `PaymentTermsTypeID` directly, and to Orders that is simply rung 1 or 2.
26
+ * Orders has no knowledge of contracts and should not grow any; "stated" is the whole interface.
27
+ *
28
+ * WHICH IS WHY STATED HAS TO BE RECORDED AS STATED. A due date that was supplied and one that was
29
+ * derived are the same value in the same column, and the difference only shows up on the next save —
30
+ * when a recompute silently moves a date somebody negotiated. `PricingBehavior` already draws this
31
+ * distinction for a stated unit price; this follows it.
32
+ *
33
+ * UNLIKE D5, IT DOES NOT FAIL LOUDLY. An unresolvable GL account means booked money with nowhere to
34
+ * go, so refusing is right. Missing terms have a sane answer — due on receipt — and refusing an
35
+ * order because nobody configured a lookup would be hostile for no gain.
36
+ *
37
+ * @module @mj-biz-apps/orders-core-entities-server
38
+ */
39
+ import { ToISODate } from '@mj-biz-apps/orders-entities';
40
+ /**
41
+ * ISO date arithmetic that does not drift across a DST boundary.
42
+ *
43
+ * Takes the cell in whatever shape it arrived. The previous form split `String(iso)` on '-', which
44
+ * on a `Date` finds nothing to split and yields `NaN` — so every date-effective terms row silently
45
+ * stopped applying and the order fell through to the company default. It failed safe rather than
46
+ * wrong, but it still restated what a customer had negotiated.
47
+ */
48
+ function dayNumber(cell) {
49
+ const iso = ToISODate(cell);
50
+ if (!iso)
51
+ return Number.NaN;
52
+ const [y, m, d] = iso.split('-').map(Number);
53
+ if (!y || !m || !d)
54
+ return Number.NaN;
55
+ return Date.UTC(y, m - 1, d);
56
+ }
57
+ /** `from` plus `days`, as `YYYY-MM-DD`. */
58
+ export function AddDays(from, days) {
59
+ const base = dayNumber(from);
60
+ if (Number.isNaN(base))
61
+ return null;
62
+ return new Date(base + days * 86_400_000).toISOString().slice(0, 10);
63
+ }
64
+ /**
65
+ * Whether a customer-terms row applies to this order.
66
+ *
67
+ * DATE-EFFECTIVE ON THE ORDER DATE, not on today. Renegotiating terms must not restate what an old
68
+ * order was due on — an order placed under Net 30 stays due when it was always due, however the
69
+ * relationship changed afterwards.
70
+ *
71
+ * A row scoped to a company applies only to that company's orders; an unscoped row applies to any.
72
+ */
73
+ export function CustomerTermsApply(row, orderDate, companyID) {
74
+ if (row.Status !== 'Active')
75
+ return false;
76
+ if (row.CompanyID && companyID && row.CompanyID.toLowerCase() !== companyID.toLowerCase())
77
+ return false;
78
+ // A row scoped to a company cannot apply to an order with no company at all.
79
+ if (row.CompanyID && !companyID)
80
+ return false;
81
+ const on = dayNumber(orderDate);
82
+ if (Number.isNaN(on))
83
+ return false;
84
+ if (row.StartedAt && on < dayNumber(row.StartedAt))
85
+ return false;
86
+ // `EndedAt` is exclusive: terms that ended on the 1st do not cover an order placed on the 1st.
87
+ if (row.EndedAt && on >= dayNumber(row.EndedAt))
88
+ return false;
89
+ return true;
90
+ }
91
+ /**
92
+ * The most specific applicable customer terms.
93
+ *
94
+ * COMPANY-SCOPED BEATS UNSCOPED, because a subsidiary that negotiated its own terms meant to
95
+ * override the group's. Among equally specific rows the one that started most recently wins — a
96
+ * later negotiation supersedes an earlier one — and a row with no start date is treated as having
97
+ * always applied, so it loses to any dated row.
98
+ */
99
+ export function BestCustomerTerms(rows, orderDate, companyID) {
100
+ const applicable = rows.filter((r) => CustomerTermsApply(r, orderDate, companyID));
101
+ if (!applicable.length)
102
+ return null;
103
+ return applicable.reduce((best, row) => {
104
+ const bestScoped = best.CompanyID ? 1 : 0;
105
+ const rowScoped = row.CompanyID ? 1 : 0;
106
+ if (rowScoped !== bestScoped)
107
+ return rowScoped > bestScoped ? row : best;
108
+ const bestStart = best.StartedAt ? dayNumber(best.StartedAt) : Number.NEGATIVE_INFINITY;
109
+ const rowStart = row.StartedAt ? dayNumber(row.StartedAt) : Number.NEGATIVE_INFINITY;
110
+ return rowStart > bestStart ? row : best;
111
+ });
112
+ }
113
+ /**
114
+ * Walk the rungs and produce the due date.
115
+ *
116
+ * Never throws and never returns "unknown": every order gets a date or an explicit due-on-receipt,
117
+ * which is the order date itself.
118
+ */
119
+ export function ResolveDueDate(input) {
120
+ // 1. Stated wins outright. Recomputing what somebody negotiated is the failure this rung exists
121
+ // to prevent, and it is also how a contracts app supplies an answer Orders cannot derive.
122
+ if (input.StatedDueDate) {
123
+ return {
124
+ DueDate: ToISODate(input.StatedDueDate),
125
+ PaymentTermsTypeID: input.StatedPaymentTermsTypeID ?? null,
126
+ Source: 'StatedDueDate',
127
+ WasStated: true,
128
+ };
129
+ }
130
+ // 2. Stated terms — the caller chose the terms and wants the date derived from them.
131
+ if (input.StatedPaymentTermsTypeID) {
132
+ const terms = input.TermsByID.get(input.StatedPaymentTermsTypeID.toLowerCase());
133
+ if (terms) {
134
+ return {
135
+ DueDate: AddDays(input.OrderDate, terms.NetDays ?? 0),
136
+ PaymentTermsTypeID: terms.PaymentTermsTypeID,
137
+ Source: 'StatedTerms',
138
+ WasStated: false,
139
+ };
140
+ }
141
+ // Terms named but unresolvable: fall through rather than refuse. The id is still recorded on
142
+ // the order, so the mistake is visible without holding up the sale.
143
+ }
144
+ // 3. What this buyer negotiated.
145
+ const customer = BestCustomerTerms(input.CustomerTerms, input.OrderDate, input.CompanyID);
146
+ if (customer) {
147
+ return {
148
+ DueDate: AddDays(input.OrderDate, customer.NetDays ?? 0),
149
+ PaymentTermsTypeID: customer.PaymentTermsTypeID,
150
+ Source: 'CustomerTerms',
151
+ WasStated: false,
152
+ };
153
+ }
154
+ // 4. What the selling company does by default.
155
+ if (input.CompanyDefault) {
156
+ return {
157
+ DueDate: AddDays(input.OrderDate, input.CompanyDefault.NetDays ?? 0),
158
+ PaymentTermsTypeID: input.CompanyDefault.PaymentTermsTypeID,
159
+ Source: 'CompanyDefault',
160
+ WasStated: false,
161
+ };
162
+ }
163
+ // 5. Due on receipt. An explicit answer rather than a null, so the collections worklist has
164
+ // something to age against on every order rather than silently skipping the unconfigured ones.
165
+ return {
166
+ DueDate: AddDays(input.OrderDate, 0),
167
+ PaymentTermsTypeID: null,
168
+ Source: 'DueOnReceipt',
169
+ WasStated: false,
170
+ };
171
+ }
172
+ //# sourceMappingURL=PaymentTermsBehavior.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PaymentTermsBehavior.js","sourceRoot":"","sources":["../src/PaymentTermsBehavior.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * PaymentWebhookHandler — the unauthenticated door, and everything that makes it safe to open.
3
+ *
4
+ * THE ROUTE HAS NO AUTHENTICATION, by necessity (D19). Stripe will not present a bearer token; the
5
+ * signature IS the credential. Every guard below exists because of that.
6
+ *
7
+ * TRANSPORT-AGNOSTIC ON PURPOSE. This is a function over `(rawBody, headers)` returning a status and a
8
+ * body, not an Express handler. Two reasons. It is unit-testable without standing up a server, which
9
+ * matters because the interesting cases are a forged signature and a replayed event — neither of which
10
+ * you want to be exercising through HTTP. And the host application owns its middleware order: the route
11
+ * must be mounted BEFORE the auth middleware and with a raw-body parser, and only the bootstrap knows
12
+ * how to do that. `MountPaymentWebhook` at the bottom is the thin adapter.
13
+ *
14
+ * THE RAW BODY IS LOAD-BEARING. The signature covers the exact bytes sent. A JSON round-trip — key
15
+ * order, whitespace, unicode normalisation — invalidates it, so any parser that runs before this
16
+ * silently breaks every webhook. `express.raw({ type: 'application/json' })` on this path only.
17
+ *
18
+ * WHAT THE RESPONSE SAYS, AND WHY IT SAYS SO LITTLE. A 2xx means "we will not need this again". A
19
+ * gateway retries anything else, so:
20
+ *
21
+ * 200 applied, or ALREADY applied, or deliberately ignored — all three are settled
22
+ * 400 malformed or unverifiable — retrying will not help, so do not ask again
23
+ * 500 we failed to record a valid event — PLEASE retry, this one is ours
24
+ *
25
+ * The body carries no detail. A public endpoint that explains why a signature failed is an oracle for
26
+ * anyone probing it, and the reason belongs in our logs where it is useful and not in a response where
27
+ * it is a hint.
28
+ *
29
+ * CONNECTS TO:
30
+ * PURE: ./PaymentProviderBehavior.ts — signature, idempotency decision
31
+ * LOOKUP: ./PaymentProviderResolver.ts
32
+ * DOC: plans/archive/bizapps-orders-master.md D19
33
+ */
34
+ import { IMetadataProvider, UserInfo } from '@memberjunction/core';
35
+ import { type WebhookAction } from './PaymentProviderBehavior.js';
36
+ export interface WebhookRequest {
37
+ /** The EXACT bytes received. See the header — a parsed-and-restringified body will not verify. */
38
+ RawBody: string;
39
+ Headers: Record<string, string | undefined>;
40
+ /** Which configured provider this endpoint is for, from the route path. */
41
+ PaymentProviderID: string;
42
+ }
43
+ export interface WebhookResponse {
44
+ Status: 200 | 400 | 500;
45
+ /** Deliberately terse — see the header. */
46
+ Body: {
47
+ received: boolean;
48
+ outcome?: WebhookAction;
49
+ };
50
+ }
51
+ /**
52
+ * Handle one delivery.
53
+ *
54
+ * The order of operations is the security model, and it is deliberate:
55
+ *
56
+ * 1. resolve the provider — so we know which secret to verify against
57
+ * 2. VERIFY THE SIGNATURE — before the payload is parsed, let alone trusted
58
+ * 3. parse — now that we know it came from the gateway
59
+ * 4. decide idempotently — before anything is written
60
+ * 5. apply — inside a transaction
61
+ *
62
+ * Nothing before step 2 reads the payload. Parsing first would mean acting on attacker-controlled JSON
63
+ * to decide whether to trust attacker-controlled JSON.
64
+ */
65
+ export declare function HandlePaymentWebhook(request: WebhookRequest, provider: IMetadataProvider, user: UserInfo): Promise<WebhookResponse>;
66
+ /**
67
+ * Mount the route on an Express-shaped app.
68
+ *
69
+ * Typed structurally rather than against `express`, so this package takes no dependency on it — the
70
+ * host already has one, and this file should not be the reason a shared server package pulls in a web
71
+ * framework.
72
+ *
73
+ * TWO THINGS THE CALLER MUST GET RIGHT, and neither can be enforced from here:
74
+ *
75
+ * · Mount BEFORE the auth middleware. Stripe presents no token; auth would reject every delivery.
76
+ * · Give this path a RAW body parser and nothing else. `express.json()` anywhere upstream re-encodes
77
+ * the payload and every signature fails.
78
+ *
79
+ * ```ts
80
+ * app.post(
81
+ * '/webhooks/payments/:providerId',
82
+ * express.raw({ type: 'application/json' }),
83
+ * MountPaymentWebhook(() => ({ provider: Metadata.Provider, user: systemUser })),
84
+ * );
85
+ * ```
86
+ */
87
+ export declare function MountPaymentWebhook(context: () => {
88
+ provider: IMetadataProvider;
89
+ user: UserInfo;
90
+ }): (req: WebhookHttpRequest, res: WebhookHttpResponse) => Promise<void>;
91
+ /** The Express request surface this route actually uses. Structural, so no dependency is needed. */
92
+ export interface WebhookHttpRequest {
93
+ body?: unknown;
94
+ headers?: Record<string, string | undefined>;
95
+ params?: Record<string, string | undefined>;
96
+ }
97
+ /** Likewise for the response. */
98
+ export interface WebhookHttpResponse {
99
+ status(code: number): {
100
+ json(body: unknown): void;
101
+ };
102
+ }
103
+ //# sourceMappingURL=PaymentWebhookHandler.d.ts.map