@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,656 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ /**
8
+ * PaymentHeader server subclass — books the PROCESSING FEE when a payment is captured (plan D18).
9
+ *
10
+ * WHAT THIS BOOKS, AND WHY IT IS ONLY THE FEE
11
+ * Capture used to book the whole cash leg here: `Dr Cash / Dr Fee / Cr AR` against the receiving
12
+ * company. That was wrong as soon as an order carried another company's product — it credited the
13
+ * collector's receivable for money the collector was never owed, and left the owner's receivable
14
+ * open forever. Balanced, posted, and invisible (D13; intercompany-balancing.md §1).
15
+ *
16
+ * The cash and receivable sides moved to `PaymentLineEntityServer`, because ALLOCATION is the
17
+ * earliest point at which the owning companies are known at all. A capture says how much cash
18
+ * arrived; only an allocation says whose revenue it settles.
19
+ *
20
+ * The fee stays here because it genuinely is a header fact: the processor takes its cut from the
21
+ * payment as a whole, not from any one order, and pro-rating it across allocations would invent
22
+ * precision the underlying fact does not have.
23
+ *
24
+ * WHEN IT BOOKS
25
+ * On the transition INTO `Captured`, once, and only when there IS a fee with an account to book it
26
+ * to. `JournalEntryID` is NULL→value-once and is checked first, so a re-save never books twice.
27
+ *
28
+ * ── THE ALLOCATION INVARIANT (D68) ─────────────────────────────────────────────────────────────
29
+ * `Amount` MUST equal the sum of the payment's lines. A payment that says 1000 arrived while its
30
+ * allocations total 600 is internally inconsistent, and the missing 400 has no home in the ledger —
31
+ * which is how "unapplied cash" became a concept that needed inventing. Requiring the two to agree
32
+ * removes it: every dollar lands on an order, and allocating MORE than an order is worth simply
33
+ * drives that order's balance negative, which is a customer credit and spendable as tender.
34
+ *
35
+ * The invariant is enforced at the CAPTURE TRANSITION, not on every save. A `Pending` payment is a
36
+ * draft — half-entered, correctable, booking nothing — exactly as a `Draft` order may sit with no
37
+ * lines. Locking on save rather than on status would make an ordinary typo permanent.
38
+ *
39
+ * Lines are held in memory on `Lines` and saved with the header inside ONE transaction (the shape
40
+ * `OrderEntityServer` uses), so there is never a persisted moment where the header and its lines
41
+ * disagree. After capture both are frozen — the header by trigger 51005, the lines by 51010/51011.
42
+ *
43
+ * ATOMICITY
44
+ * The entry is written inside the caller's transaction, exactly as order booking is. A payment that
45
+ * fails to book does not persist as captured.
46
+ *
47
+ * REFUNDS reverse. A payment saved as `Refunded` books the mirror of its capture (D53): same
48
+ * accounts, debit and credit swapped, positive amounts.
49
+ *
50
+ * CONNECTS TO:
51
+ * FACTORY: PaymentJournalEntryFactory (./PaymentJournalEntryFactory.ts)
52
+ * ALLOCATION: PaymentLineEntityServer (./PaymentLineEntityServer.ts) — the cash/AR side
53
+ * RESOLVER: GLAccountResolver (./GLAccountResolver.ts)
54
+ * OP: 'Accounting.CreateJournalEntries' — the same op OrderEntityServer uses
55
+ * TABLE: __mj_BizAppsOrders.PaymentHeader
56
+ */
57
+ import { BaseEntity, BaseEntityResult, BaseRemotableOperation, EntitySaveOptions, LogError, LogStatus, RunView, ValidationErrorInfo, ValidationErrorType, } from '@memberjunction/core';
58
+ import { MJGlobal, RegisterClass } from '@memberjunction/global';
59
+ import { PaymentHeaderEntity } from '@mj-biz-apps/orders-entities';
60
+ import { AccountingEngineBase } from '@mj-biz-apps/accounting-engine-base';
61
+ import { BuildGLAccountResolver, EntityIDFor } from './AccountingBridge.js';
62
+ import { ResolvePaymentProvider } from './PaymentProviderResolver.js';
63
+ import { ShouldHoldForLateSettlement, SplitCapturedAmount } from './PaymentProviderBehavior.js';
64
+ import { PaymentJournalEntryFactory } from './PaymentJournalEntryFactory.js';
65
+ import { PaymentAllocationFactory, } from './PaymentAllocationFactory.js';
66
+ import { LoadOrdersEngine, OrdersEngine } from '@mj-biz-apps/orders-entities';
67
+ import { ORDER_HEADER_ENTITY, ORDER_LINE_ENTITY } from './entity-names.js';
68
+ const PAYMENT_HEADER_ENTITY = 'MJ_BizApps_Orders: Payment Headers';
69
+ const PAYMENT_LINE_ENTITY = 'MJ_BizApps_Orders: Payment Lines';
70
+ /** Statuses whose entry belongs in the ledger. */
71
+ const BOOKED_STATUSES = new Set(['Captured', 'Refunded']);
72
+ let PaymentHeaderEntityServer = class PaymentHeaderEntityServer extends PaymentHeaderEntity {
73
+ // `Lines` is a RelatedRecordCollection on the GENERATED class now, emitted from the
74
+ // RelatedRecordCollection metadata on the 'Payment Headers -> Payment Lines' relationship, so it
75
+ // exists on both tiers. It replaces a `_lines` array plus getter/setter that lived only on the
76
+ // server. No Sequence policy is declared: an allocation is identified by which order line it
77
+ // pays, not by position, and PaymentLine has no line-number column to renumber.
78
+ Validate() {
79
+ const result = super.Validate();
80
+ // A fee larger than the payment would put a negative amount into Cash.
81
+ const fee = this.ProcessingFeeAmount ?? 0;
82
+ if (fee < 0 || fee > Math.abs(this.Amount ?? 0)) {
83
+ result.Success = false;
84
+ result.Errors.push(new ValidationErrorInfo('ProcessingFeeAmount', `The processing fee (${fee}) must be between 0 and the payment amount ` +
85
+ `(${this.Amount}). A larger fee would book negative cash.`, fee, ValidationErrorType.Failure));
86
+ }
87
+ return result;
88
+ }
89
+ async Save(options) {
90
+ if (!this.PaymentNumber) {
91
+ this.PaymentNumber = await this.assignPaymentNumber();
92
+ }
93
+ // A rail that settles on somebody else's schedule may not reach Captured here, whoever asked.
94
+ await this.deferCaptureWhenSettlementIsLate();
95
+ const capturing = this.willBookOnThisSave();
96
+ // A Pending payment with no new lines is an ordinary row save — nothing to co-ordinate.
97
+ if (!capturing && !this.Lines.Dirty) {
98
+ return super.Save(options);
99
+ }
100
+ const dbProvider = this.ProviderToUse;
101
+ await dbProvider.BeginTransaction();
102
+ try {
103
+ // SETTLE WITH THE GATEWAY BEFORE THE ROW IS WRITTEN (D19).
104
+ //
105
+ // Two reasons, and the second is the one that bit. If the ledger were written first and the
106
+ // gateway then declined, the rollback would undo our entries — but a gateway that CAPTURED
107
+ // and then failed to answer has taken the customer's money while we recorded nothing.
108
+ //
109
+ // And the gateway is the authority on the AMOUNT and the FEE: a partial capture, or a fee
110
+ // different from the one assumed, is what actually moved. Those land on THIS object, so
111
+ // they have to be set before `super.Save()` persists it. Calling this afterwards computed
112
+ // the right fee and threw it away — the row kept its zero, and the only reason anyone
113
+ // noticed was that PV4 asserted the fee rather than just the status.
114
+ if (capturing)
115
+ await this.settleWithProvider();
116
+ // COMPLETE THE ALLOCATIONS BEFORE THE HEADER SAVE, because `Lines` is a companion and MJ
117
+ // validates companions from the PARENT's save — before any line's own Save() runs.
118
+ //
119
+ // `AllocatedAt` is NOT NULL and used to be defaulted during the line-save loop, which ran
120
+ // after the header. Most callers set it (CapturePaymentOperation, PaymentReversalFactory,
121
+ // OrderEntityServer) but the fallback exists precisely for those that do not, and without
122
+ // it here their allocations fail validation on a column no caller is required to author.
123
+ //
124
+ // The foreign key is NOT set here — the collection stamps PaymentHeaderID itself, which is
125
+ // what makes this correct even when the header is new and its key is minted by this save.
126
+ for (const line of this.Lines.Items) {
127
+ if (!line.AllocatedAt)
128
+ line.AllocatedAt = new Date();
129
+ }
130
+ // THE HEADER + EMBEDS, then the allocations explicitly.
131
+ //
132
+ // Letting the graph write the lines reads better and IS what the journal-entry path
133
+ // does — but it fails here, and the suite says so: `assertAllocationInvariant` and
134
+ // `bookAllocations` both read PaymentLine FROM THE DATABASE by PaymentHeaderID inside
135
+ // this transaction.
136
+ for (const line of this.Lines.Items) {
137
+ line.PaymentHeaderID = this.ID;
138
+ }
139
+ if (!(await super.Save({ ...options, SkipRelatedCollections: true }))) {
140
+ throw new Error(`Failed to save payment ${this.PaymentNumber}: ` +
141
+ `${this.LatestResult?.CompleteMessage ?? 'unknown error'}`);
142
+ }
143
+ // The lines go down BEFORE the invariant is checked, because the check reads what is
144
+ // actually persisted rather than what this object happens to be holding — a line that
145
+ // silently failed to save would otherwise still count toward the total.
146
+ for (const line of this.Lines.Items) {
147
+ const lineSaveOptions = new EntitySaveOptions();
148
+ if (options)
149
+ Object.assign(lineSaveOptions, options);
150
+ lineSaveOptions.IsParentEntitySave = true;
151
+ if (!(await line.Save(lineSaveOptions))) {
152
+ throw new Error(`Failed to save a payment allocation for ${this.PaymentNumber}: ` +
153
+ `${line.LatestResult?.CompleteMessage ?? 'unknown error'}`);
154
+ }
155
+ }
156
+ if (capturing) {
157
+ // Book journal entries for all unbooked allocations of this payment in one atomic batch.
158
+ await this.bookAllocations(options);
159
+ await this.assertAllocationInvariant();
160
+ // Only reach the fee builder when there IS a fee AND this tender books one inline.
161
+ // OFF BY DEFAULT (D82).
162
+ if (Number(this.ProcessingFeeAmount ?? 0) > 0 && (await this.feeBooksInline())) {
163
+ await this.bookProcessingFee(options);
164
+ }
165
+ }
166
+ await dbProvider.CommitTransaction();
167
+ return true;
168
+ }
169
+ catch (err) {
170
+ LogError(`PaymentHeaderEntityServer.Save failed for ${this.PaymentNumber ?? this.ID}: ${err}`);
171
+ try {
172
+ await dbProvider.RollbackTransaction();
173
+ }
174
+ catch (rollbackErr) {
175
+ LogError(`Rollback failed after payment save error: ${rollbackErr}`);
176
+ }
177
+ // Same reasoning as OrderEntityServer (D50): a bare `false` with LatestResult still
178
+ // holding the successful row save reads as "it just didn't work".
179
+ this.RegisterResultHistoryEntry(this.buildFailureResult(err));
180
+ return false;
181
+ }
182
+ }
183
+ /**
184
+ * True when this save needs to book journal entries.
185
+ * Evaluates true on a new record being saved as Captured/Refunded, on status transitions into
186
+ * Captured/Refunded, or when there are unbooked lines attached.
187
+ */
188
+ willBookOnThisSave() {
189
+ if (!BOOKED_STATUSES.has(this.Status))
190
+ return false;
191
+ if (!this.IsSaved)
192
+ return true;
193
+ const previousStatus = this.GetFieldByName('Status')?.OldValue;
194
+ if (previousStatus !== this.Status)
195
+ return true;
196
+ if (this.Lines.Items.some((l) => !l.BookedAt))
197
+ return true;
198
+ return false;
199
+ }
200
+ /**
201
+ * A payment on an asynchronously-settling rail lands `Pending`, whatever the caller wrote.
202
+ *
203
+ * WHY THIS IS HERE AND NOT ONLY IN `Orders.CapturePayment`. The operation already asks the driver
204
+ * this question and sets the status accordingly, and that was the whole of the rule — which made
205
+ * it a rule the OPERATION follows rather than one the system enforces. Every other way of writing
206
+ * a payment went straight past it: a workflow saving a `PaymentHeader`, a UI form, a migration, a
207
+ * test builder that defaults `Status` to `'Captured'`. Each of those books `Dr Cash` for a bank
208
+ * debit that has not cleared, and the entry is perfectly balanced, so nothing downstream can tell.
209
+ * Finance sees cash four days before the bank does, and the only symptom is a reconciliation that
210
+ * is off by whatever ACH ran that week.
211
+ *
212
+ * This is the last shared point before booking, so it is the honest place for the invariant. The
213
+ * operation's copy stays — it needs the answer BEFORE the save so it can report the status it is
214
+ * about to produce — and the two now agree by construction rather than by both remembering.
215
+ *
216
+ * THE PROMOTION MUST STILL PASS. When the webhook moves a `Pending` payment to `Captured`, that IS
217
+ * the bank answering, and deferring it again would mean a bank debit could never book at all. The
218
+ * signal is the PERSISTED status: a row that was already `Pending` is being promoted; a new row,
219
+ * or one arriving from any other state, is a caller declaring cash that has not moved.
220
+ *
221
+ * SILENT ON PURPOSE, AND ONLY HERE. It changes the status rather than refusing the save, because
222
+ * `Pending` is what the caller should have written and refusing would strand a payment the gateway
223
+ * has already been told about. `Orders.CapturePayment` reports the resulting status back, so a
224
+ * caller going through the front door is told plainly what happened.
225
+ */
226
+ async deferCaptureWhenSettlementIsLate() {
227
+ if (this.Status !== 'Captured')
228
+ return;
229
+ // The webhook promoting a Pending payment — the bank has answered, so this one books.
230
+ const previousStatus = this.GetFieldByName('Status')?.OldValue;
231
+ if (this.IsSaved && previousStatus === 'Pending')
232
+ return;
233
+ const providerID = this.PaymentProviderID;
234
+ // No gateway means a RECORDED payment — cheque, cash, wire. Nothing settles late.
235
+ if (!providerID)
236
+ return;
237
+ let settlesLate = false;
238
+ try {
239
+ const driver = await ResolvePaymentProvider(providerID, this.ProviderToUse, this.ContextCurrentUser);
240
+ settlesLate = driver.SettlesAsynchronously === true;
241
+ }
242
+ catch {
243
+ // A provider that will not resolve is a configuration fault, and `settleWithProvider` is
244
+ // about to report it far more precisely than a status guess here would. Leaving the
245
+ // status alone keeps every rail that worked before working unchanged.
246
+ return;
247
+ }
248
+ if (!ShouldHoldForLateSettlement({
249
+ RequestedStatus: this.Status,
250
+ PersistedStatus: previousStatus,
251
+ IsSaved: this.IsSaved,
252
+ HasProvider: true,
253
+ SettlesAsynchronously: settlesLate,
254
+ })) {
255
+ return;
256
+ }
257
+ LogStatus(`Payment ${this.PaymentNumber}: this provider settles asynchronously, so the payment is ` +
258
+ `held at Pending and its allocations persist unbooked. The webhook books the cash leg ` +
259
+ `when the bank confirms.`);
260
+ this.Status = 'Pending';
261
+ }
262
+ /**
263
+ * Persist the in-memory allocation lines against this payment.
264
+ *
265
+ * `PaymentLineEntityServer` books each one's journal entries as it saves, and it reads the
266
+ * payment's status to decide whether to — so by the time these run the header is already
267
+ * `Captured` and the cash leg follows automatically.
268
+ */
269
+ /**
270
+ * Book one journal entry per company owning a line on the orders settled by unbooked allocations.
271
+ *
272
+ * In the payment-header save, all allocations for the payment are gathered and drafted together,
273
+ * submitted to Accounting.CreateJournalEntries in one batch inside the payment transaction, and
274
+ * stamped with BookedAt.
275
+ */
276
+ async bookAllocations(options) {
277
+ const provider = this.ProviderToUse;
278
+ const user = this.ContextCurrentUser;
279
+ const unbookedLines = [];
280
+ const seenLineIDs = new Set();
281
+ for (const line of this.Lines.Items) {
282
+ if (!line.BookedAt) {
283
+ unbookedLines.push(line);
284
+ if (line.ID)
285
+ seenLineIDs.add(String(line.ID).toLowerCase());
286
+ }
287
+ }
288
+ if (this.ID) {
289
+ const rv = new RunView(this.ProviderToUse);
290
+ const res = await rv.RunView({
291
+ EntityName: PAYMENT_LINE_ENTITY,
292
+ ExtraFilter: `PaymentHeaderID='${this.ID}' AND BookedAt IS NULL`,
293
+ ResultType: 'entity_object',
294
+ BypassCache: true,
295
+ }, this.ContextCurrentUser);
296
+ for (const line of (res?.Results ?? [])) {
297
+ if (!seenLineIDs.has(String(line.ID).toLowerCase())) {
298
+ unbookedLines.push(line);
299
+ seenLineIDs.add(String(line.ID).toLowerCase());
300
+ }
301
+ }
302
+ }
303
+ if (unbookedLines.length === 0)
304
+ return;
305
+ await AccountingEngineBase.Instance.Config(false, user, provider);
306
+ const resolver = await BuildGLAccountResolver(provider, user);
307
+ const factory = new PaymentAllocationFactory(resolver, (source, target, asOf) => {
308
+ const hit = AccountingEngineBase.Instance.ResolveIntercompanyAccounts(source, target, asOf);
309
+ return hit ? { DueToGLAccountID: hit.DueTo.GLAccountID, DueFromGLAccountID: hit.DueFrom.GLAccountID } : null;
310
+ }, EntityIDFor(PAYMENT_LINE_ENTITY));
311
+ const allDrafts = [];
312
+ for (const line of unbookedLines) {
313
+ const orderLines = await this.loadOrderLines(line.OrderHeaderID);
314
+ if (orderLines.length === 0) {
315
+ throw new Error(`Cannot allocate ${line.Amount} to order ${line.OrderHeaderID}: the order has no lines, so ` +
316
+ `there is no basis for deciding whose revenue the cash settles.`);
317
+ }
318
+ const orderNumber = await this.loadOrderNumber(line.OrderHeaderID);
319
+ const { Drafts } = await factory.BuildAllocationDrafts({
320
+ PaymentLineID: line.ID,
321
+ PaymentNumber: this.PaymentNumber,
322
+ OrderNumber: orderNumber,
323
+ Amount: line.Amount ?? 0,
324
+ ReceivingCompanyID: this.ReceivingCompanyID,
325
+ OrderLines: orderLines,
326
+ TargetOrderLineID: line.OrderLineID ?? null,
327
+ PaymentDate: this.PaymentDate ? new Date(this.PaymentDate) : new Date(),
328
+ IsReversal: this.Status === 'Refunded' || (line.Amount ?? 0) < 0,
329
+ });
330
+ allDrafts.push(...Drafts);
331
+ }
332
+ if (allDrafts.length === 0)
333
+ return;
334
+ const result = await this.createJournalEntries(allDrafts, provider, user);
335
+ if ((result.Results?.length ?? 0) !== allDrafts.length) {
336
+ throw new Error(`Accounting returned ${result.Results?.length ?? 0} journal entries for allocations that ` +
337
+ `produced ${allDrafts.length} drafts. The cash leg is incomplete; refusing to commit.`);
338
+ }
339
+ for (const line of unbookedLines) {
340
+ line.BookedAt = new Date();
341
+ const saveOptions = new EntitySaveOptions();
342
+ if (options)
343
+ Object.assign(saveOptions, options);
344
+ saveOptions.IsParentEntitySave = true;
345
+ if (!(await line.Save(saveOptions))) {
346
+ throw new Error(`Failed to stamp BookedAt on the allocation: ${line.LatestResult?.CompleteMessage ?? 'unknown error'}`);
347
+ }
348
+ }
349
+ }
350
+ async loadOrderLines(orderHeaderID) {
351
+ const rv = new RunView(this.ProviderToUse);
352
+ const res = await rv.RunView({
353
+ EntityName: ORDER_LINE_ENTITY,
354
+ ExtraFilter: `OrderHeaderID='${orderHeaderID}'`,
355
+ Fields: ['ID', 'CompanyID', 'LineTotalGross'],
356
+ ResultType: 'simple',
357
+ BypassCache: true,
358
+ }, this.ContextCurrentUser);
359
+ if (!res?.Success) {
360
+ throw new Error(`Could not read the order's lines to allocate the payment: ${res?.ErrorMessage ?? 'unknown error'}`);
361
+ }
362
+ return (res.Results ?? []).map((l) => ({
363
+ OrderLineID: l.ID,
364
+ CompanyID: l.CompanyID,
365
+ Amount: Number(l.LineTotalGross ?? 0),
366
+ }));
367
+ }
368
+ async loadOrderNumber(orderHeaderID) {
369
+ const rv = new RunView(this.ProviderToUse);
370
+ const res = await rv.RunView({
371
+ EntityName: ORDER_HEADER_ENTITY,
372
+ ExtraFilter: `ID='${orderHeaderID}'`,
373
+ Fields: ['OrderNumber'],
374
+ ResultType: 'simple',
375
+ BypassCache: true,
376
+ }, this.ContextCurrentUser);
377
+ return res?.Results?.[0]?.OrderNumber ?? String(orderHeaderID);
378
+ }
379
+ /**
380
+ * `Amount` must equal the sum of the payment's persisted lines (D68).
381
+ *
382
+ * Checked at CAPTURE only. A `Pending` payment is a draft and may be as inconsistent as a
383
+ * half-typed form; capture is the moment it becomes a claim about money that actually moved,
384
+ * and from then on the header and lines are both frozen.
385
+ *
386
+ * Read from the database rather than from `this._lines`: that is the number the ledger and
387
+ * every downstream report will see.
388
+ */
389
+ async assertAllocationInvariant() {
390
+ const rv = new RunView(this.ProviderToUse);
391
+ const res = await rv.RunView({
392
+ EntityName: PAYMENT_LINE_ENTITY,
393
+ ExtraFilter: `PaymentHeaderID='${this.ID}'`,
394
+ Fields: ['Amount'],
395
+ ResultType: 'simple',
396
+ BypassCache: true,
397
+ }, this.ContextCurrentUser);
398
+ if (!res?.Success) {
399
+ throw new Error(`Could not read the allocations for payment ${this.PaymentNumber}: ${res?.ErrorMessage ?? 'unknown error'}`);
400
+ }
401
+ const rows = res.Results ?? [];
402
+ const allocated = Math.round(rows.reduce((sum, l) => sum + Number(l.Amount ?? 0), 0) * 100) / 100;
403
+ const amount = Math.round(Number(this.Amount ?? 0) * 100) / 100;
404
+ // DIRECTION. `Amount` is stored as a positive magnitude on both a capture and a refund —
405
+ // "how much moved" — while the LINES are signed by which way it moved: positive applies cash
406
+ // to an order, negative takes it back off. So a refund of 250 is Amount 250 with lines
407
+ // totalling -250, and comparing the two naively would fail every refund ever written.
408
+ // `Status === 'Refunded'` is the same discriminator the booking path already uses to decide
409
+ // whether to mirror the entry (D53), so the two stay in step by construction.
410
+ const reversal = this.Status === 'Refunded';
411
+ const expected = reversal ? -amount : amount;
412
+ if (allocated === expected)
413
+ return;
414
+ const shortfall = Math.round((expected - allocated) * 100) / 100;
415
+ const verb = reversal ? 'refunded' : 'captured';
416
+ throw new Error(`Payment ${this.PaymentNumber} cannot be ${verb}: it is for ${amount} but its ` +
417
+ `${rows.length} allocation${rows.length === 1 ? '' : 's'} total ${allocated} ` +
418
+ `(expected ${expected}), leaving ${shortfall} unaccounted for. Every part of a payment must ` +
419
+ `land on an order — allocating MORE than an order is worth is allowed and simply leaves that ` +
420
+ `order with a credit balance, which can be spent on another order later. Adjust the ` +
421
+ `allocations, or record the payment for what actually moved.`);
422
+ }
423
+ /**
424
+ * Ask the gateway to actually take the money, and record what it says happened.
425
+ *
426
+ * NO PROVIDER, NO CALL. `PaymentProviderID` is nullable, and a payment without one is a payment
427
+ * nobody is asking a gateway about — a back-office correction, an account-credit transfer (D68), a
428
+ * historical import. Those capture exactly as they did before this method existed, which is why it
429
+ * returns early rather than refusing: requiring a provider would break every one of them.
430
+ *
431
+ * ALREADY SETTLED IS NOT RE-SETTLED. A payment carrying a `ProviderChargeID` has been through the
432
+ * gateway once. Re-saving it for any reason must not charge the customer again, so the presence of
433
+ * that reference is the idempotency key — the same shape as `JournalEntryID` for booking.
434
+ *
435
+ * THE GATEWAY'S NUMBERS WIN. `Amount` and `ProcessingFeeAmount` are overwritten from the capture
436
+ * result, because what moved is what moved. `SplitCapturedAmount` then checks the three reconcile
437
+ * before the fee leg is built, so a nonsensical fee is refused here rather than producing an entry
438
+ * that will not balance three calls later.
439
+ */
440
+ async settleWithProvider() {
441
+ const providerID = this.PaymentProviderID;
442
+ if (!providerID)
443
+ return;
444
+ const alreadySettled = this.ProviderChargeID;
445
+ if (alreadySettled)
446
+ return;
447
+ const provider = this.ProviderToUse;
448
+ const user = this.ContextCurrentUser;
449
+ // THE GATEWAY'S INTENT STRING LIVES ON `PaymentIntent`, NOT HERE. `PaymentHeader.PaymentIntentID`
450
+ // is a foreign key to our row; `PaymentIntent.ProviderIntentID` is what Stripe calls it. Reading
451
+ // a `ProviderIntentID` off the header would compile — every column access here is a cast, since
452
+ // the generated base does not declare them — and be `undefined` at run time, refusing every
453
+ // provider-backed capture with a message about a missing intent.
454
+ const intent = await this.loadProviderIntent();
455
+ if (!intent) {
456
+ throw new Error(`Payment ${this.PaymentNumber} names a payment provider but no provider intent, so there ` +
457
+ `is nothing for the gateway to capture. Open an intent first, or clear ` +
458
+ `PaymentProviderID for a payment that is being recorded rather than collected.`);
459
+ }
460
+ const driver = await ResolvePaymentProvider(providerID, provider, user);
461
+ const capture = await driver.Capture({
462
+ ProviderIntentID: intent,
463
+ Amount: this.Amount ?? 0,
464
+ CurrencyCode: await this.functionalCurrency(),
465
+ });
466
+ if (!capture.Success) {
467
+ // A REFUSAL, not a fault — the card was declined, the intent was in the wrong state. It
468
+ // still fails the save, because a payment that did not capture must not be Captured; but it
469
+ // fails with the gateway's own words, which is what the person retrying needs.
470
+ throw new Error(`The gateway refused to capture payment ${this.PaymentNumber}: ` +
471
+ `${capture.Reason ?? 'no reason given'}`);
472
+ }
473
+ // UNDEFINED means the driver could not determine a fee, which is NOT the same as no fee (see
474
+ // BasePaymentProvider). Keeping whatever was already on the row is the honest response: we do
475
+ // not know, so we do not overwrite what somebody may have entered.
476
+ const gross = capture.Amount ?? this.Amount ?? 0;
477
+ if (capture.FeeAmount != null) {
478
+ const split = SplitCapturedAmount(gross, capture.FeeAmount);
479
+ this.Amount = split.Gross;
480
+ this.ProcessingFeeAmount = split.Fee;
481
+ this.NetAmount = split.Net;
482
+ }
483
+ else {
484
+ this.Amount = Math.round(gross * 100) / 100;
485
+ }
486
+ if (capture.ProviderChargeID) {
487
+ this.ProviderChargeID = capture.ProviderChargeID;
488
+ }
489
+ }
490
+ /** The gateway's own intent string, from the `PaymentIntent` row this payment points at. */
491
+ async loadProviderIntent() {
492
+ const intentID = this.PaymentIntentID;
493
+ if (!intentID)
494
+ return null;
495
+ const rv = new RunView(this.ProviderToUse);
496
+ const result = await rv.RunView({
497
+ EntityName: 'MJ_BizApps_Orders: Payment Intents',
498
+ ExtraFilter: `ID = '${intentID}'`,
499
+ ResultType: 'simple',
500
+ }, this.ContextCurrentUser);
501
+ return result?.Results?.[0]?.ProviderIntentID ?? null;
502
+ }
503
+ /**
504
+ * The currency to transact in.
505
+ *
506
+ * THERE IS NO CURRENCY COLUMN ON A PAYMENT, deliberately (MOD-4): these tables are single-currency,
507
+ * and the currency is a property of the COMPANY collecting the money. Reading it from the receiving
508
+ * company's accounting profile keeps that true rather than quietly introducing a second opinion.
509
+ *
510
+ * Defaults to USD only when the profile cannot be read, because a gateway call needs *some* code and
511
+ * refusing the capture over a missing profile row would be a worse failure than the wrong exponent —
512
+ * which, for USD against any other two-decimal currency, is not even wrong.
513
+ */
514
+ async functionalCurrency() {
515
+ const rv = new RunView(this.ProviderToUse);
516
+ const result = await rv.RunView({
517
+ EntityName: 'MJ_BizApps_Accounting: Accounting Company Profiles',
518
+ ExtraFilter: `ID = '${this.ReceivingCompanyID}'`,
519
+ ResultType: 'simple',
520
+ }, this.ContextCurrentUser);
521
+ return result?.Results?.[0]?.FunctionalCurrencyCode ?? 'USD';
522
+ }
523
+ /**
524
+ * Whether this payment's TENDER books its processor fee as its own ledger leg (D82).
525
+ *
526
+ * READ FROM `PaymentType.BookProcessingFeeInline`, which defaults to 0 for every tender. The fee
527
+ * is still read from the gateway and still stored on this row; this decides only whether it
528
+ * becomes a journal entry. See the migration for why off is the correct default rather than the
529
+ * timid one — a per-payment fee leg cannot reconcile to a statement, because the processor
530
+ * batches into payouts and deducts costs that never attach to any payment.
531
+ *
532
+ * A MISSING TENDER ANSWERS FALSE, LOUDLY. Booking a fee we could not justify is worse than
533
+ * omitting one the month-end accrual will pick up anyway, and refusing the whole capture over it
534
+ * would hold up real money for a reporting detail. This is the same posture `bookProcessingFee`
535
+ * already takes when no Processing Fee account is linked: say so plainly, book the payment, move on.
536
+ *
537
+ * READ FROM THE ENGINE CACHE, NOT A PER-CALL QUERY. This used to be a `RunView` naming the single
538
+ * column, which reads as a careful minimal query and is really a hidden dependency on CodeGen
539
+ * having run: `Fields` names a column that must exist in the base view and be registered as an
540
+ * `EntityField`, and when it is not, `RunView` does not throw — it returns unsuccessfully, this
541
+ * method takes its defensive branch, and the switch is silently stuck off. Payment types are a
542
+ * lookup of eleven rows read on every capture; they belong in {@link OrdersEngine}, where a
543
+ * missing field fails the load once at startup instead of every call site guessing a default.
544
+ */
545
+ async feeBooksInline() {
546
+ await LoadOrdersEngine(this.ProviderToUse, this.ContextCurrentUser);
547
+ const tender = OrdersEngine.Instance.PaymentTypeByID(this.PaymentTypeID);
548
+ if (!tender) {
549
+ LogError(`Payment ${this.PaymentNumber}: payment type ${this.PaymentTypeID} is not in the orders ` +
550
+ `lookup cache, so whether this tender books its fee inline is unknown. No fee entry ` +
551
+ `will be booked; the processor cost is accrued at month end.`);
552
+ return false;
553
+ }
554
+ return tender.BookProcessingFeeInline === true;
555
+ }
556
+ async bookProcessingFee(options) {
557
+ const provider = this.ProviderToUse;
558
+ const user = this.ContextCurrentUser;
559
+ const factory = new PaymentJournalEntryFactory(await BuildGLAccountResolver(provider, user), EntityIDFor(PAYMENT_HEADER_ENTITY));
560
+ const { Draft, UnbookedFeeAmount } = await factory.BuildCaptureDraft({
561
+ PaymentID: this.ID,
562
+ PaymentNumber: this.PaymentNumber,
563
+ CompanyID: this.ReceivingCompanyID,
564
+ Amount: this.Amount ?? 0,
565
+ ProcessingFeeAmount: this.ProcessingFeeAmount ?? 0,
566
+ PaymentDate: this.PaymentDate ? new Date(this.PaymentDate) : new Date(),
567
+ IsReversal: this.Status === 'Refunded',
568
+ });
569
+ if (UnbookedFeeAmount) {
570
+ // Not fatal — the allocation entries still book the gross to Cash. But the bank
571
+ // position is then overstated by this much, so it must not pass silently.
572
+ LogError(`Payment ${this.PaymentNumber}: processing fee of ${UnbookedFeeAmount} was NOT booked ` +
573
+ `because no 'Processing Fee' GL account is linked for company ` +
574
+ `${this.ReceivingCompanyID}. Cash will read gross by that amount.`);
575
+ }
576
+ // No fee, or no account for it: there is nothing for the HEADER to book. The cash and
577
+ // receivable sides belong to the allocation (PaymentLineEntityServer), which is the
578
+ // earliest point the owning companies are known.
579
+ if (!Draft)
580
+ return;
581
+ const result = await this.createJournalEntries([Draft], provider, user);
582
+ const journalEntryID = result.Results?.[0]?.JournalEntryID;
583
+ if (!journalEntryID) {
584
+ throw new Error(`Accounting returned no journal entry for payment ${this.PaymentNumber}.`);
585
+ }
586
+ // NULL→value-once; the DB trigger enforces the same rule independently.
587
+ this.JournalEntryID = journalEntryID;
588
+ if (!(await super.Save(options))) {
589
+ throw new Error(`Failed to stamp JournalEntryID on payment ${this.PaymentNumber}: ` +
590
+ `${this.LatestResult?.CompleteMessage ?? 'unknown error'}`);
591
+ }
592
+ }
593
+ async createJournalEntries(drafts, provider, user) {
594
+ const op = MJGlobal.Instance.ClassFactory.CreateInstance(BaseRemotableOperation, 'Accounting.CreateJournalEntries');
595
+ if (!op) {
596
+ throw new Error(`The 'Accounting.CreateJournalEntries' operation is not registered. The BizApps ` +
597
+ `Accounting server package must be loaded before payments can book the cash leg.`);
598
+ }
599
+ const result = await op.Execute({ Drafts: drafts }, { provider, user });
600
+ if (!result.Success) {
601
+ throw new Error(`Accounting.CreateJournalEntries did not execute: ${result.ErrorMessage ?? result.ResultCode ?? 'unknown error'}`);
602
+ }
603
+ const payload = result.Output;
604
+ if (!payload) {
605
+ throw new Error('Accounting.CreateJournalEntries returned no payload.');
606
+ }
607
+ if (!payload.Success) {
608
+ const detail = (payload.Errors ?? [])
609
+ .map((e) => `${e.Code ?? 'ERROR'}: ${e.Message ?? ''}`)
610
+ .join('; ');
611
+ throw new Error(`Journal entry booking failed for payment ${this.PaymentNumber}. ${detail}`);
612
+ }
613
+ return payload;
614
+ }
615
+ async assignPaymentNumber() {
616
+ return `PAY-${String(await this.nextSequence('PaymentSequence')).padStart(6, '0')}`;
617
+ }
618
+ async nextSequence(table) {
619
+ const provider = this.ProviderToUse;
620
+ const rows = (await provider.ExecuteSQL(`DECLARE @seq TABLE (Seq INT);
621
+ UPDATE __mj_BizAppsOrders.${table} WITH (UPDLOCK, HOLDLOCK)
622
+ SET NextSequenceNumber = NextSequenceNumber + 1
623
+ OUTPUT deleted.NextSequenceNumber INTO @seq(Seq)
624
+ WHERE ID = 1;
625
+ SELECT Seq FROM @seq;`));
626
+ const list = Array.isArray(rows) ? rows : (rows?.Results ?? []);
627
+ const val = list[0]?.Seq;
628
+ if (typeof val !== 'number' || val <= 0) {
629
+ throw new Error(`Failed to assign sequence number from ${table}: unexpected return ${JSON.stringify(rows)}`);
630
+ }
631
+ return val;
632
+ }
633
+ buildFailureResult(err) {
634
+ // Same shape as OrderEntityServer's (D50): a bare `false` leaves LatestResult holding the
635
+ // row's SUCCESSFUL save, so the caller sees no reason for the refusal.
636
+ const result = new BaseEntityResult();
637
+ result.Success = false;
638
+ result.Type = this.IsSaved ? 'update' : 'create';
639
+ result.Message = err instanceof Error ? err.message : String(err);
640
+ result.Error = err;
641
+ result.OriginalValues = this.Fields.map((f) => ({ FieldName: f.Name, Value: f.OldValue }));
642
+ result.NewValues = this.Fields.map((f) => ({ FieldName: f.Name, Value: f.Value }));
643
+ result.StartedAt = new Date();
644
+ result.EndedAt = new Date();
645
+ return result;
646
+ }
647
+ };
648
+ PaymentHeaderEntityServer = __decorate([
649
+ RegisterClass(BaseEntity, PAYMENT_HEADER_ENTITY)
650
+ ], PaymentHeaderEntityServer);
651
+ export { PaymentHeaderEntityServer };
652
+ /** Tree-shaking anchor — booking lives in Save, which is only reachable via the registration. */
653
+ export function LoadPaymentHeaderEntityServer() {
654
+ // intentionally empty
655
+ }
656
+ //# sourceMappingURL=PaymentHeaderEntityServer.js.map