@mj-biz-apps/orders-entities 5.18.0 → 5.20.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 (45) hide show
  1. package/dist/ExternalPaymentBehavior.d.ts +123 -0
  2. package/dist/ExternalPaymentBehavior.d.ts.map +1 -0
  3. package/dist/ExternalPaymentBehavior.js +217 -0
  4. package/dist/ExternalPaymentBehavior.js.map +1 -0
  5. package/dist/OrderHeaderEntity.d.ts +48 -2
  6. package/dist/OrderHeaderEntity.d.ts.map +1 -1
  7. package/dist/OrderHeaderEntity.js +105 -5
  8. package/dist/OrderHeaderEntity.js.map +1 -1
  9. package/dist/OrderLineEntity.d.ts +5 -0
  10. package/dist/OrderLineEntity.d.ts.map +1 -1
  11. package/dist/OrderLineEntity.js +13 -0
  12. package/dist/OrderLineEntity.js.map +1 -1
  13. package/dist/PaymentHeaderEntity.d.ts +17 -1
  14. package/dist/PaymentHeaderEntity.d.ts.map +1 -1
  15. package/dist/PaymentHeaderEntity.js +37 -1
  16. package/dist/PaymentHeaderEntity.js.map +1 -1
  17. package/dist/booked-money.d.ts +5 -1
  18. package/dist/booked-money.d.ts.map +1 -1
  19. package/dist/booked-money.js +5 -0
  20. package/dist/booked-money.js.map +1 -1
  21. package/dist/generated/entities/__mj_BizAppsOrders.d.ts +1314 -87
  22. package/dist/generated/entities/__mj_BizAppsOrders.d.ts.map +1 -1
  23. package/dist/generated/entities/__mj_BizAppsOrders.js +1916 -108
  24. package/dist/generated/entities/__mj_BizAppsOrders.js.map +1 -1
  25. package/dist/generated/remote_operations.d.ts +412 -0
  26. package/dist/generated/remote_operations.d.ts.map +1 -1
  27. package/dist/generated/remote_operations.js +126 -0
  28. package/dist/generated/remote_operations.js.map +1 -1
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/order-address-snapshot.d.ts +52 -0
  34. package/dist/order-address-snapshot.d.ts.map +1 -0
  35. package/dist/order-address-snapshot.js +62 -0
  36. package/dist/order-address-snapshot.js.map +1 -0
  37. package/dist/pricing/OrdersEngine.d.ts +22 -2
  38. package/dist/pricing/OrdersEngine.d.ts.map +1 -1
  39. package/dist/pricing/OrdersEngine.js +45 -1
  40. package/dist/pricing/OrdersEngine.js.map +1 -1
  41. package/dist/pricing/priceOverride.d.ts +8 -0
  42. package/dist/pricing/priceOverride.d.ts.map +1 -1
  43. package/dist/pricing/priceOverride.js +12 -0
  44. package/dist/pricing/priceOverride.js.map +1 -1
  45. package/package.json +1 -1
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @fileoverview Pure decisions for money that arrives on an external rail (Bill.com), read by
3
+ * `Orders.PollExternalPayments`. No I/O, no entities — every rule here is a table or a function of
4
+ * its arguments, so it can be tested exhaustively and read by a finance reviewer.
5
+ *
6
+ * THREE RULES THAT ARE DELIBERATE:
7
+ *
8
+ * UNKNOWN STATUS HOLDS. Bill.com's `receivable-payments.status` vocabulary is not in the
9
+ * connector's catalog; the table below is what the sandbox showed (spike S2) plus the obvious
10
+ * spellings. A status not in the table is HELD and surfaced, never read as cleared — a payment
11
+ * recorded on a guess is a ledger entry for money that may not exist.
12
+ *
13
+ * ALL-OR-NOTHING FAN-OUT. One Bill.com payment can settle many invoices. If any of them is not one
14
+ * we issued, the WHOLE payment is Unmatched and nothing is captured — a partial capture would
15
+ * misstate the payment and hide the unmatched remainder behind a plausible-looking record.
16
+ *
17
+ * MATCHING IS BY INVOICE ID ONLY. Never by amount, never by `OrderHeader.ExternalDocumentNumber`
18
+ * (free-form, hand-editable, not unique — golive #146/#148).
19
+ *
20
+ * @module @mj-biz-apps/orders-entities
21
+ */
22
+ export type ExternalPaymentDisposition = 'Captured' | 'Held' | 'Unmatched' | 'Refused' | 'Ignored' | 'Reapplied' | 'ReversalNeeded';
23
+ export type RailPaymentClass = 'Cleared' | 'Pending' | 'Reversed';
24
+ /**
25
+ * Bill.com `receivable-payments.status` → what it means for cash. Keys are upper-cased.
26
+ *
27
+ * THIS IS THE WHOLE ENUM, not a guess: BILL documents exactly six values for
28
+ * `ReceivablePaymentResponseDto.status`, and `PAID` on an offline check was confirmed live in the
29
+ * sandbox on 2026-09-22 (spike S2). An earlier provisional table carried `CLEARED`, `PROCESSING`,
30
+ * `PENDING`, `VOIDED`, `CANCELLED`, `SETTLED`, `COMPLETED`, `IN_PROCESS`, `FAILED` and `RETURNED` —
31
+ * none of which BILL can emit. They are gone rather than kept "just in case", because a key that
32
+ * cannot occur is a claim about the vendor that no one can check.
33
+ *
34
+ * `UNDEFINED` is deliberately absent so it falls to `Unknown` → Hold: BILL saying it does not know
35
+ * is not a reason for us to decide.
36
+ *
37
+ * `ESCHEATED` means the funds went unclaimed and were remitted to the state. The money is not coming,
38
+ * so it classes with the reversals: never captured, and if we already captured it, surfaced as
39
+ * `ReversalNeeded` for a person.
40
+ */
41
+ export declare const BILLCOM_PAYMENT_STATUS: Readonly<Record<string, RailPaymentClass>>;
42
+ export declare function ClassifyPaymentStatus(status: string | null | undefined): RailPaymentClass | 'Unknown';
43
+ export interface PaymentSeen {
44
+ ExternalPaymentRef: string;
45
+ Status: string | null;
46
+ /**
47
+ * True when the rail's current `invoicePayments[]` no longer match the payment lines we hold for
48
+ * this payment; false when they do. Computed by the caller, which needs the ledger.
49
+ *
50
+ * `undefined` means COULD NOT TELL — not checked, or the read behind the comparison failed. It is
51
+ * deliberately distinct from `false`: on a payment already flagged `Reapplied`, treating "could not
52
+ * tell" as "no difference" would retire a live exception on one bad database read, with the wrong
53
+ * allocation still in place.
54
+ */
55
+ ApplicationsChanged?: boolean;
56
+ /** What the last pass did with this payment, or null when it has never been seen. */
57
+ PriorDisposition: ExternalPaymentDisposition | null;
58
+ }
59
+ export type ExternalPaymentAction = 'Capture' | 'Hold' | 'Ignore' | 'Reapplied' | 'ReversalNeeded';
60
+ export interface ExternalPaymentDecision {
61
+ Action: ExternalPaymentAction;
62
+ Reason: string;
63
+ /**
64
+ * What the `ExternalPayment` row should now READ AS, when that differs from keeping what it says.
65
+ *
66
+ * `Action` is what to do; this is what to store, and the two part company in exactly one place: a
67
+ * payment flagged `Reapplied` whose allocation has since been corrected needs no action but must
68
+ * stop reading as an exception. Without it the poller's `Ignore` branch wrote the prior
69
+ * disposition straight back, so the row sat on the queue page for ever, labelled an exception with
70
+ * a reason saying there was nothing wrong.
71
+ */
72
+ Disposition?: ExternalPaymentDisposition;
73
+ /**
74
+ * True when this answer rests on a comparison that could not be made, so the reason already on the
75
+ * row — which described a difference somebody can act on — is worth more than this one.
76
+ */
77
+ KeepPriorReason?: boolean;
78
+ }
79
+ export declare function DecideExternalPayment(p: PaymentSeen): ExternalPaymentDecision;
80
+ /** What the poller needs to know about the unit behind one of our external invoices. */
81
+ export interface UnitRef {
82
+ OrderHeaderID: string;
83
+ CompanyID: string;
84
+ OrderHeaderPaymentScheduleID: string | null;
85
+ BillToOrganizationID: string | null;
86
+ BillToPersonID: string | null;
87
+ }
88
+ export interface InvoiceAllocation {
89
+ OrderHeaderID: string;
90
+ Amount: number;
91
+ OrderHeaderPaymentScheduleID: string | null;
92
+ }
93
+ export type AllocationOutcome = {
94
+ OK: true;
95
+ Allocations: InvoiceAllocation[];
96
+ Payer: {
97
+ BillToOrganizationID: string | null;
98
+ BillToPersonID: string | null;
99
+ };
100
+ CompanyID: string;
101
+ Total: number;
102
+ } | {
103
+ OK: false;
104
+ Unmatched: string[];
105
+ Reason: string;
106
+ };
107
+ /**
108
+ * Turn Bill.com's `invoicePayments[]` into `Orders.CapturePayment` allocations, or explain why not.
109
+ * The lookup answers "which unit did we issue this invoice for" from `ExternalInvoice`.
110
+ */
111
+ export declare function AllocateInvoicePayments(invoicePayments: ReadonlyArray<{
112
+ ExternalInvoiceRef: string;
113
+ Amount: number;
114
+ }>, lookup: (externalInvoiceRef: string) => UnitRef | undefined): AllocationOutcome;
115
+ /** `PaymentHeader.IdempotencyKey` for a rail payment — the D19 guard, enforced by UX_PaymentHeader_IdempotencyKey. */
116
+ export declare function ExternalPaymentIdempotencyKey(typeCode: string, externalPaymentRef: string): string;
117
+ /** The `PaymentType.Code` values this rail can produce. All seeded and active in `metadata/payment-types`. */
118
+ export type OrdersTenderCode = 'ACH' | 'Cash' | 'Check' | 'CreditCard' | 'Wire';
119
+ export declare function TenderFor(p: {
120
+ OnlinePayment: boolean | null;
121
+ ReceivablesType: string | null;
122
+ }): OrdersTenderCode;
123
+ //# sourceMappingURL=ExternalPaymentBehavior.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExternalPaymentBehavior.d.ts","sourceRoot":"","sources":["../src/ExternalPaymentBehavior.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,217 @@
1
+ /**
2
+ * @fileoverview Pure decisions for money that arrives on an external rail (Bill.com), read by
3
+ * `Orders.PollExternalPayments`. No I/O, no entities — every rule here is a table or a function of
4
+ * its arguments, so it can be tested exhaustively and read by a finance reviewer.
5
+ *
6
+ * THREE RULES THAT ARE DELIBERATE:
7
+ *
8
+ * UNKNOWN STATUS HOLDS. Bill.com's `receivable-payments.status` vocabulary is not in the
9
+ * connector's catalog; the table below is what the sandbox showed (spike S2) plus the obvious
10
+ * spellings. A status not in the table is HELD and surfaced, never read as cleared — a payment
11
+ * recorded on a guess is a ledger entry for money that may not exist.
12
+ *
13
+ * ALL-OR-NOTHING FAN-OUT. One Bill.com payment can settle many invoices. If any of them is not one
14
+ * we issued, the WHOLE payment is Unmatched and nothing is captured — a partial capture would
15
+ * misstate the payment and hide the unmatched remainder behind a plausible-looking record.
16
+ *
17
+ * MATCHING IS BY INVOICE ID ONLY. Never by amount, never by `OrderHeader.ExternalDocumentNumber`
18
+ * (free-form, hand-editable, not unique — golive #146/#148).
19
+ *
20
+ * @module @mj-biz-apps/orders-entities
21
+ */
22
+ /**
23
+ * Bill.com `receivable-payments.status` → what it means for cash. Keys are upper-cased.
24
+ *
25
+ * THIS IS THE WHOLE ENUM, not a guess: BILL documents exactly six values for
26
+ * `ReceivablePaymentResponseDto.status`, and `PAID` on an offline check was confirmed live in the
27
+ * sandbox on 2026-09-22 (spike S2). An earlier provisional table carried `CLEARED`, `PROCESSING`,
28
+ * `PENDING`, `VOIDED`, `CANCELLED`, `SETTLED`, `COMPLETED`, `IN_PROCESS`, `FAILED` and `RETURNED` —
29
+ * none of which BILL can emit. They are gone rather than kept "just in case", because a key that
30
+ * cannot occur is a claim about the vendor that no one can check.
31
+ *
32
+ * `UNDEFINED` is deliberately absent so it falls to `Unknown` → Hold: BILL saying it does not know
33
+ * is not a reason for us to decide.
34
+ *
35
+ * `ESCHEATED` means the funds went unclaimed and were remitted to the state. The money is not coming,
36
+ * so it classes with the reversals: never captured, and if we already captured it, surfaced as
37
+ * `ReversalNeeded` for a person.
38
+ */
39
+ export const BILLCOM_PAYMENT_STATUS = Object.freeze({
40
+ PAID: 'Cleared',
41
+ SCHEDULED: 'Pending',
42
+ VOID: 'Reversed',
43
+ CANCELED: 'Reversed',
44
+ ESCHEATED: 'Reversed',
45
+ });
46
+ export function ClassifyPaymentStatus(status) {
47
+ if (!status)
48
+ return 'Unknown';
49
+ return BILLCOM_PAYMENT_STATUS[status.trim().toUpperCase()] ?? 'Unknown';
50
+ }
51
+ export function DecideExternalPayment(p) {
52
+ const cls = ClassifyPaymentStatus(p.Status);
53
+ const ref = p.ExternalPaymentRef;
54
+ // A CAPTURED PAYMENT AND A RE-APPLIED ONE ARE THE SAME QUESTION, asked a poll apart. Both have
55
+ // cash recorded against them, so both are re-examined the same way — and `Reapplied` is NOT a
56
+ // terminal disposition (it is deliberately absent from the poller's FINAL set, so every pass
57
+ // re-decides it). Falling through to the status switch, as this used to, answered `Capture` for a
58
+ // cleared payment: the capture returned `WasRetry` on the idempotency key, the poller wrote
59
+ // `Captured` back over the row, and the exception a person was supposed to act on vanished an
60
+ // hour after it was raised — leaving the wrong allocation in place with nothing flagging it.
61
+ if (p.PriorDisposition === 'Captured' || p.PriorDisposition === 'Reapplied') {
62
+ if (cls === 'Reversed') {
63
+ return { Action: 'ReversalNeeded', Reason: `${ref} was captured and Bill.com now reports it '${p.Status}'. The cash has to be reversed (bank-return path, O-US13); nothing is changed automatically.` };
64
+ }
65
+ // THE RAIL CAN MOVE MONEY WE ALREADY RECORDED. Bill.com lets Finance re-apply a receipt:
66
+ // 700.00 recorded against invoice A last week becomes 300.00 on A and 400.00 on B today, or
67
+ // moves to invoice C entirely. The payment is unchanged in total, so nothing here is wrong —
68
+ // but our PaymentLines now point at the wrong invoices, Orders shows B open while Bill.com
69
+ // shows it paid, and no amount of re-polling notices because the payment is "already
70
+ // captured". Reported for a person rather than silently ignored, the same posture a void
71
+ // after capture takes.
72
+ const alreadyFlagged = p.PriorDisposition === 'Reapplied';
73
+ if (p.ApplicationsChanged === true) {
74
+ return {
75
+ Action: 'Reapplied',
76
+ Reason: alreadyFlagged
77
+ ? `${ref} is still applied differently on Bill.com from the payment lines held here; the allocation has not been corrected.`
78
+ : `${ref} was captured, and Bill.com has since applied it to different invoices. The cash is right; the allocation here is not. Re-allocate it by hand — nothing is changed automatically.`,
79
+ };
80
+ }
81
+ // COULD NOT TELL is not ALL CLEAR. `undefined` means the comparison was not made, or the read
82
+ // behind it failed. On a row already flagged that must hold the flag AND the words on it: they
83
+ // describe a real difference somebody can act on, and one bad database read must not retire it.
84
+ // A merely captured row is unaffected — there is nothing to hold.
85
+ if (alreadyFlagged && p.ApplicationsChanged === undefined) {
86
+ return {
87
+ Action: 'Reapplied',
88
+ Reason: `${ref} is flagged re-applied and the applications could not be compared on this pass; the flag stands until they can be.`,
89
+ KeepPriorReason: true,
90
+ };
91
+ }
92
+ // THE EXCEPTION CLEARS ITSELF once the allocation matches again, so a person who re-allocates
93
+ // by hand does not also have to come back and retire the flag. Storing `Captured` is the point:
94
+ // leaving `Reapplied` on the row kept it on the queue page for ever, labelled an exception,
95
+ // under a reason saying there was nothing wrong.
96
+ if (alreadyFlagged) {
97
+ return {
98
+ Action: 'Ignore',
99
+ Disposition: 'Captured',
100
+ Reason: `${ref} was re-applied on Bill.com and the allocation here now matches again; the exception is cleared.`,
101
+ };
102
+ }
103
+ return { Action: 'Ignore', Reason: `${ref} is already captured.` };
104
+ }
105
+ if (p.PriorDisposition === 'ReversalNeeded') {
106
+ return { Action: 'Ignore', Reason: `${ref} is already flagged for reversal.` };
107
+ }
108
+ if (p.PriorDisposition === 'Ignored') {
109
+ // Terminal by a person's hand (an Unmatched or Refused row set to Ignored), or a reversal that
110
+ // was never captured. Either way, nothing here re-opens it — a status flip on the rail cannot
111
+ // make pre-cutover AR ours.
112
+ return { Action: 'Ignore', Reason: `${ref} was set aside (Ignored); nothing is captured for it.` };
113
+ }
114
+ switch (cls) {
115
+ case 'Cleared':
116
+ return { Action: 'Capture', Reason: `${ref} is '${p.Status}': cleared and not yet captured.` };
117
+ case 'Pending':
118
+ return { Action: 'Hold', Reason: `${ref} is '${p.Status}': promised, not yet banked. Held until Bill.com reports it cleared.` };
119
+ case 'Reversed':
120
+ return { Action: 'Ignore', Reason: `${ref} is '${p.Status}' and was never captured; there is nothing to reverse.` };
121
+ default:
122
+ return {
123
+ Action: 'Hold',
124
+ Reason: `${ref} has an unknown Bill.com status '${p.Status ?? '(none)'}'. Held rather than guessed — add the status to BILLCOM_PAYMENT_STATUS once its meaning is confirmed.`,
125
+ };
126
+ }
127
+ }
128
+ const money = (n) => Math.round((Number(n) + Number.EPSILON) * 100) / 100;
129
+ /**
130
+ * Turn Bill.com's `invoicePayments[]` into `Orders.CapturePayment` allocations, or explain why not.
131
+ * The lookup answers "which unit did we issue this invoice for" from `ExternalInvoice`.
132
+ */
133
+ export function AllocateInvoicePayments(invoicePayments, lookup) {
134
+ if (!invoicePayments.length) {
135
+ return { OK: false, Unmatched: [], Reason: 'The payment is applied to no invoice on Bill.com; there is no order to apply it to here.' };
136
+ }
137
+ const unmatched = [];
138
+ const allocations = [];
139
+ const companies = new Set();
140
+ let payer = null;
141
+ for (const ip of invoicePayments) {
142
+ const unit = lookup(ip.ExternalInvoiceRef);
143
+ if (!unit) {
144
+ unmatched.push(ip.ExternalInvoiceRef);
145
+ continue;
146
+ }
147
+ const amount = money(ip.Amount);
148
+ if (!(amount > 0)) {
149
+ return { OK: false, Unmatched: [], Reason: `Invoice ${ip.ExternalInvoiceRef} carries a non-positive share (${ip.Amount}); refusing to allocate it.` };
150
+ }
151
+ allocations.push({ OrderHeaderID: unit.OrderHeaderID, Amount: amount, OrderHeaderPaymentScheduleID: unit.OrderHeaderPaymentScheduleID });
152
+ companies.add(unit.CompanyID.toLowerCase());
153
+ // Orders may carry BOTH a bill-to organisation and a person (no XOR on OrderHeader); the
154
+ // capture needs exactly one payer, and the organisation is the customer (D65).
155
+ payer ??= unit.BillToOrganizationID
156
+ ? { BillToOrganizationID: unit.BillToOrganizationID, BillToPersonID: null }
157
+ : { BillToOrganizationID: null, BillToPersonID: unit.BillToPersonID };
158
+ }
159
+ if (unmatched.length) {
160
+ return {
161
+ OK: false,
162
+ Unmatched: unmatched,
163
+ Reason: `Bill.com invoice(s) ${unmatched.join(', ')} were not issued by Orders (pre-cutover AR, or created directly in Bill.com). Nothing captured: a partial capture would misstate the payment.`,
164
+ };
165
+ }
166
+ if (companies.size > 1) {
167
+ return { OK: false, Unmatched: [], Reason: `The payment settles invoices for ${companies.size} receiving companies; one payment must land in one company's books.` };
168
+ }
169
+ const total = money(allocations.reduce((s, a) => s + a.Amount, 0));
170
+ return { OK: true, Allocations: allocations, Payer: payer, CompanyID: allocations.length ? lookup(invoicePayments[0].ExternalInvoiceRef).CompanyID : '', Total: total };
171
+ }
172
+ /** `PaymentHeader.IdempotencyKey` for a rail payment — the D19 guard, enforced by UX_PaymentHeader_IdempotencyKey. */
173
+ export function ExternalPaymentIdempotencyKey(typeCode, externalPaymentRef) {
174
+ return `${typeCode.toLowerCase()}:${externalPaymentRef}`;
175
+ }
176
+ /**
177
+ * Bill.com `receivablesType` → an Orders `PaymentType.Code`, for `Orders.CapturePayment`.
178
+ *
179
+ * `receivablesType` is the authoritative signal and is read FIRST. An earlier version short-circuited
180
+ * on `OnlinePayment === true` and answered ACH, which would have booked an online card payment to the
181
+ * bank: BILL reports the method on online payments too. `OnlinePayment` now only breaks the tie when
182
+ * BILL reports no type at all, where "BILL moved it" is the one thing we do know.
183
+ *
184
+ * The keys are BILL's whole documented enum. Note what is NOT in it: there is no `WIRE`. `CASH` maps
185
+ * to Orders' own `Cash` type, which the old default silently booked as ACH.
186
+ *
187
+ * `OTHER` MEANS WIRE, BY AGREEMENT — not by inference. Wires do arrive (money landing directly in the
188
+ * bank is marked paid in Bill.com by hand), and BILL has no wire type, so whoever marks the invoice
189
+ * must pick something from the enum. Jeremy settled this on PR #235 on 2026-09-25, choosing the
190
+ * convention the spec offered as option A (§13 A): Finance selects `Other` when marking a wire paid,
191
+ * and `OTHER` maps to Orders' `Wire`. It is safe to spend `OTHER` this way precisely because Finance
192
+ * confirmed AIDP uses no PayPal, wallet or other tender, so nothing else can legitimately claim it.
193
+ *
194
+ * The convention is deliberately NOT a marker read out of the description or reference number: that
195
+ * would depend on consistent typing by a person, which is exactly what this kind of rule must not do.
196
+ *
197
+ * `PAYPAL`, `WALLET` and `UNDEFINED` are left falling to ACH. Finance says they cannot occur, so this
198
+ * is unreachable rather than a guess — but if one ever did arrive it would book to the bank silently,
199
+ * which is the same shape of defect the rest of this table exists to remove. Worth revisiting the day
200
+ * the residue stops being hypothetical; it is not worth inventing a refusal path for today.
201
+ */
202
+ const BILLCOM_TENDER = Object.freeze({
203
+ CASH: 'Cash',
204
+ CHECK: 'Check',
205
+ CREDIT_CARD: 'CreditCard',
206
+ VIRTUAL_CARD: 'CreditCard',
207
+ ACH: 'ACH',
208
+ // By agreement, not by inference — see above.
209
+ OTHER: 'Wire',
210
+ });
211
+ export function TenderFor(p) {
212
+ const mapped = BILLCOM_TENDER[(p.ReceivablesType ?? '').trim().toUpperCase()];
213
+ if (mapped)
214
+ return mapped;
215
+ return 'ACH';
216
+ }
217
+ //# sourceMappingURL=ExternalPaymentBehavior.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExternalPaymentBehavior.js","sourceRoot":"","sources":["../src/ExternalPaymentBehavior.ts"],"names":[],"mappings":"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"}
@@ -34,6 +34,7 @@ import { mjBizAppsOrdersOrderHeaderEntity } from './generated/entity_subclasses.
34
34
  import { type TransitionVerdict } from './OrderStatusBehavior.js';
35
35
  import { PromotionCodesCompanion } from './PromotionCodesCompanion.js';
36
36
  import { InitialPaymentIntentCompanion } from './InitialPaymentIntentCompanion.js';
37
+ import { type OrderAddressSnapshot } from './order-address-snapshot.js';
37
38
  /** The order editor's sections, in the order the screen shows them. */
38
39
  export type OrderEditorSection = 'header' | 'parties' | 'lines' | 'charges' | 'payment';
39
40
  export declare class OrderHeaderEntity extends mjBizAppsOrdersOrderHeaderEntity {
@@ -58,6 +59,10 @@ export declare class OrderHeaderEntity extends mjBizAppsOrdersOrderHeaderEntity
58
59
  ClearInitialPaymentDetail(): void;
59
60
  ClearBillToAddress(): void;
60
61
  ClearShipToAddress(): void;
62
+ /** The bill-to address this order was confirmed with, or null before it is confirmed. */
63
+ get BillToAddressAsSold(): OrderAddressSnapshot | null;
64
+ /** The ship-to address this order was confirmed with, or null before it is confirmed. */
65
+ get ShipToAddressAsSold(): OrderAddressSnapshot | null;
61
66
  get InitialPaymentReference(): string | null;
62
67
  set InitialPaymentReference(value: string | null);
63
68
  private isInitialPaymentDetailEmpty;
@@ -139,6 +144,26 @@ export declare class OrderHeaderEntity extends mjBizAppsOrdersOrderHeaderEntity
139
144
  * The booking save itself still has to write the figures it just computed.
140
145
  */
141
146
  private refuseBookedMoneyEdits;
147
+ /**
148
+ * Set by the server subclass while it writes the address snapshots itself, so the rule below can
149
+ * tell its own write from a caller's. Cleared once the save returns.
150
+ */
151
+ protected addressSnapshotsStamped: boolean;
152
+ /**
153
+ * Refuse a change to where a confirmed order was sold (golive #263).
154
+ *
155
+ * A confirmed order keeps the addresses it was sold to for the life of the order: the state a
156
+ * sale counts in is decided by where the customer was on the date of sale. An address that is
157
+ * set cannot be replaced or cleared. An empty one may be filled, as an empty bill-to party may
158
+ * be filled after confirm, and the server snapshots it on that save.
159
+ * `trg_OrderHeader_AddressFrozenAfterConfirm` (51015) and
160
+ * `trg_OrderLine_AddressFrozenAfterConfirm` (51016) hold the same rule at the database; this
161
+ * says so before the round trip, against the field that was changed.
162
+ *
163
+ * The snapshots are the server's to write, from the Address rows, so a change to one from
164
+ * anywhere else is refused on any order.
165
+ */
166
+ private refuseBookedAddressEdits;
142
167
  /**
143
168
  * Which editing SECTION each validation failure belongs to.
144
169
  *
@@ -185,8 +210,19 @@ export declare class OrderHeaderEntity extends mjBizAppsOrdersOrderHeaderEntity
185
210
  * it asks this, then calls {@link Confirm}.
186
211
  */
187
212
  ConfirmEligibility(): TransitionVerdict;
188
- /** True when {@link Confirm} is a legal next move from what is on this object. */
189
- get CanConfirm(): boolean;
213
+ /**
214
+ * Lines whose recognition type needs a service period (`RequiresServicePeriod`), that nothing
215
+ * will date, and that do not have both dates.
216
+ *
217
+ * Event lines are left out because the save stamps them from the event, and subscription lines
218
+ * because it stamps them from the term. What remains has no source but the person entering the
219
+ * order. The browser asks this before offering Confirm, so the user is told before the server's
220
+ * recognition driver refuses the booking.
221
+ *
222
+ * Reads `OrdersEngine`; the caller loads it. With the cache empty nothing counts, which leaves
223
+ * the decision to the server.
224
+ */
225
+ LinesMissingServicePeriod(): OrderHeaderEntity['Lines']['Items'];
190
226
  /**
191
227
  * Confirm — the irreversible step.
192
228
  *
@@ -199,6 +235,16 @@ export declare class OrderHeaderEntity extends mjBizAppsOrdersOrderHeaderEntity
199
235
  * missing payer without a round trip.
200
236
  */
201
237
  Confirm(): Promise<void>;
238
+ /**
239
+ * Move to `status` by saving, and put the previous status back if the save is refused.
240
+ *
241
+ * Without the restore a refused transition stays on the object as an unsaved edit: the screen
242
+ * reads the new status, `IsBookedOrder` hides the verbs that depend on it, and every later save
243
+ * re-sends the same transition and is refused the same way, whatever else changed.
244
+ *
245
+ * Throws with the server's reason.
246
+ */
247
+ SaveStatus(status: OrderHeaderEntity['Status'], fallbackMessage: string): Promise<void>;
202
248
  /**
203
249
  * Load this order and its lines together — the state an editor needs.
204
250
  *
@@ -1 +1 @@
1
- {"version":3,"file":"OrderHeaderEntity.d.ts","sourceRoot":"","sources":["../src/OrderHeaderEntity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAwE,gBAAgB,EAAW,KAAK,oBAAoB,EAAyB,MAAM,sBAAsB,CAAC;AAGzL,OAAO,EAAE,gCAAgC,EAAsC,MAAM,+BAA+B,CAAC;AACrH,OAAO,EAA4C,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAEzG,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAWhF,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;AAKxF,qBACa,iBAAkB,SAAQ,gCAAgC;IACnE;;OAEG;IACI,YAAY,CAAC,GAAG,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO;IAIrD;;;;;;;OAOG;IACH,SAAgB,cAAc,0BAA6D;IAE3F;;;OAGG;IACH,SAAgB,oBAAoB,gCAAmE;IAEhG,yBAAyB,IAAI,IAAI;IAKjC,kBAAkB,IAAI,IAAI;IAI1B,kBAAkB,IAAI,IAAI;IAIjC,IAAW,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAMlD;IAED,IAAW,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAmBtD;IAED,OAAO,CAAC,2BAA2B;IAanC;;;;;;;;;;;;;;;;OAgBG;IACH,SAAS,CAAC,eAAe,UAAS;IAElC,kFAAkF;IAClF,SAAS,CAAC,kBAAkB,IAAI,OAAO;IAOvC;;;;;;;;;;OAUG;IACH;;;;;;;;;OASG;IACH,SAAS,CAAC,uBAAuB,IAAI,iBAAiB;IAKtD,mGAAmG;IACnG,SAAS,CAAC,uBAAuB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM;IAIrE;;;;;;OAMG;IACa,SAAS,CAAC,SAAS,CAAC,EAAE,oBAAoB,GAAG,OAAO;IAapD,QAAQ,IAAI,gBAAgB;IAiF5C;;;;;;;;;OASG;IACH,OAAO,CAAC,4BAA4B;IAcpC,4DAA4D;IAC5D,IAAW,aAAa,IAAI,OAAO,CAElC;IAED;;;;;OAKG;IACH,IAAW,WAAW,IAAI,OAAO,CAIhC;IAED;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAqD9B;;;;;;;;;;;;;OAaG;WACW,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,kBAAkB;IAuBpF;;;;;OAKG;IACI,kBAAkB,IAAI,kBAAkB,EAAE;IAUjD;;;;;;;;;;;;;;OAcG;IACU,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAMzC;;;;;;OAMG;IACI,kBAAkB,IAAI,iBAAiB;IAe9C,kFAAkF;IAClF,IAAW,UAAU,IAAI,OAAO,CAE/B;IAED;;;;;;;;;;OAUG;IACU,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAUrC;;;;;;;;OAQG;IACU,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAOnE;;;;;;;;OAQG;IACU,wBAAwB,CAAC,OAAO,GAAE,QAAQ,GAAG,QAAQ,GAAG,MAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBpG;;;OAGG;IACU,gCAAgC,CACzC,SAAS,GAAE,QAAQ,GAAG,QAAmB,EACzC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GACzB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAwB5B"}
1
+ {"version":3,"file":"OrderHeaderEntity.d.ts","sourceRoot":"","sources":["../src/OrderHeaderEntity.ts"],"names":[],"mappings":"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"}
@@ -44,8 +44,10 @@ import { ResolveActiveEmployerOrganization } from './PartyAffiliationBehavior.js
44
44
  import { PromotionCodesCompanion } from './PromotionCodesCompanion.js';
45
45
  import { InitialPaymentIntentCompanion } from './InitialPaymentIntentCompanion.js';
46
46
  import { IsSavePopulatedFieldError } from './save-populated-fields.js';
47
+ import { OrdersEngine } from './pricing/OrdersEngine.js';
47
48
  import { anyFieldIsDirty } from './field-dirty.js';
48
49
  import { AsDateValue, TodayAsDateValue } from './date-cell.js';
50
+ import { ParseAddressSnapshot } from './order-address-snapshot.js';
49
51
  import { BookedMoneyEditMessage, ORDER_HEADER_MONEY_FIELDS, ORDER_HEADER_SET_ONCE_FIELDS, ORDER_LINE_MONEY_FIELDS, } from './booked-money.js';
50
52
  /** Statuses that mean the order has been booked to the ledger (plan D8). */
51
53
  const BOOKED_STATUSES = new Set(['Confirmed']);
@@ -84,6 +86,11 @@ let OrderHeaderEntity = OrderHeaderEntity_1 = class OrderHeaderEntity extends mj
84
86
  * `protected` because the server subclass sets it around its booking walk.
85
87
  */
86
88
  this.bookingInFlight = false;
89
+ /**
90
+ * Set by the server subclass while it writes the address snapshots itself, so the rule below can
91
+ * tell its own write from a caller's. Cleared once the save returns.
92
+ */
93
+ this.addressSnapshotsStamped = false;
87
94
  }
88
95
  /**
89
96
  * True when any of the named fields has unsaved changes. See {@link anyFieldIsDirty}.
@@ -101,6 +108,14 @@ let OrderHeaderEntity = OrderHeaderEntity_1 = class OrderHeaderEntity extends mj
101
108
  ClearShipToAddress() {
102
109
  this.GetCompanion('ShipToAddressID_Object')?.Clear();
103
110
  }
111
+ /** The bill-to address this order was confirmed with, or null before it is confirmed. */
112
+ get BillToAddressAsSold() {
113
+ return ParseAddressSnapshot(this.BillToAddressSnapshot);
114
+ }
115
+ /** The ship-to address this order was confirmed with, or null before it is confirmed. */
116
+ get ShipToAddressAsSold() {
117
+ return ParseAddressSnapshot(this.ShipToAddressSnapshot);
118
+ }
104
119
  get InitialPaymentReference() {
105
120
  return (this.InitialPaymentDetailID_Object?.ReferenceNumber ??
106
121
  this.InitialPaymentIntent.Reference ??
@@ -212,6 +227,7 @@ let OrderHeaderEntity = OrderHeaderEntity_1 = class OrderHeaderEntity extends mj
212
227
  const result = super.Validate();
213
228
  this.dropSavePopulatedFieldErrors(result);
214
229
  this.refuseBookedMoneyEdits(result);
230
+ this.refuseBookedAddressEdits(result);
215
231
  const verdict = this.statusTransitionVerdict();
216
232
  if (!verdict.Allowed) {
217
233
  result.Success = false;
@@ -330,6 +346,54 @@ let OrderHeaderEntity = OrderHeaderEntity_1 = class OrderHeaderEntity extends mj
330
346
  result.Success = false;
331
347
  result.Errors.push(new ValidationErrorInfo('Status', message, this.Status, ValidationErrorType.Failure));
332
348
  }
349
+ /**
350
+ * Refuse a change to where a confirmed order was sold (golive #263).
351
+ *
352
+ * A confirmed order keeps the addresses it was sold to for the life of the order: the state a
353
+ * sale counts in is decided by where the customer was on the date of sale. An address that is
354
+ * set cannot be replaced or cleared. An empty one may be filled, as an empty bill-to party may
355
+ * be filled after confirm, and the server snapshots it on that save.
356
+ * `trg_OrderHeader_AddressFrozenAfterConfirm` (51015) and
357
+ * `trg_OrderLine_AddressFrozenAfterConfirm` (51016) hold the same rule at the database; this
358
+ * says so before the round trip, against the field that was changed.
359
+ *
360
+ * The snapshots are the server's to write, from the Address rows, so a change to one from
361
+ * anywhere else is refused on any order.
362
+ */
363
+ refuseBookedAddressEdits(result) {
364
+ const refuse = (source, message) => {
365
+ result.Success = false;
366
+ result.Errors.push(new ValidationErrorInfo(source, message, null, ValidationErrorType.Failure));
367
+ };
368
+ const wasSet = (entity, name) => entity.GetFieldByName(name)?.OldValue != null;
369
+ if (!this.addressSnapshotsStamped) {
370
+ for (const name of ['BillToAddressSnapshot', 'ShipToAddressSnapshot']) {
371
+ if (this.FieldIsDirty(name)) {
372
+ refuse(name, `${name} is written by the server from the Address row and cannot be set directly.`);
373
+ }
374
+ }
375
+ this.Lines.Items.forEach((line, index) => {
376
+ if (anyFieldIsDirty(line, ['ShipToAddressSnapshot'])) {
377
+ refuse(`Lines[${index}].ShipToAddressSnapshot`, 'ShipToAddressSnapshot is written by the server from the Address row and cannot be set directly.');
378
+ }
379
+ });
380
+ }
381
+ if (!this.MoneyLocked)
382
+ return;
383
+ const order = `Order ${this.OrderNumber ?? ''}`.trim();
384
+ for (const name of ['BillToAddressID', 'ShipToAddressID']) {
385
+ if (this.FieldIsDirty(name) && wasSet(this, name)) {
386
+ refuse(name, `${order} is confirmed, so its ${name === 'BillToAddressID' ? 'bill-to' : 'ship-to'} address ` +
387
+ `cannot be replaced or cleared: the order keeps the address it was sold to. Use a reversal order.`);
388
+ }
389
+ }
390
+ this.Lines.Items.forEach((line, index) => {
391
+ if (line.IsSaved && anyFieldIsDirty(line, ['ShipToAddressID']) && wasSet(line, 'ShipToAddressID')) {
392
+ refuse(`Lines[${index}].ShipToAddressID`, `${order} is confirmed, so line ${line.LineNumber ?? index + 1}'s ship-to address cannot be ` +
393
+ `replaced or cleared: the line keeps the address it was sold to. Use a reversal order.`);
394
+ }
395
+ });
396
+ }
333
397
  /**
334
398
  * Which editing SECTION each validation failure belongs to.
335
399
  *
@@ -424,11 +488,33 @@ let OrderHeaderEntity = OrderHeaderEntity_1 = class OrderHeaderEntity extends mj
424
488
  if (this.Lines.Count === 0 && (!this.IsSaved || this.Lines.IsLoaded)) {
425
489
  return { Allowed: false, Reason: 'Need a line.' };
426
490
  }
491
+ const undated = this.LinesMissingServicePeriod();
492
+ if (undated.length > 0) {
493
+ const numbers = undated.map((l) => l.LineNumber).filter((n) => n != null);
494
+ const which = numbers.length > 0 ? `Line ${numbers.join(', ')}` : 'A line';
495
+ return {
496
+ Allowed: false,
497
+ Reason: `${which} needs a service period (start and end date) before this can be confirmed.`,
498
+ };
499
+ }
427
500
  return { Allowed: true };
428
501
  }
429
- /** True when {@link Confirm} is a legal next move from what is on this object. */
430
- get CanConfirm() {
431
- return this.ConfirmEligibility().Allowed;
502
+ /**
503
+ * Lines whose recognition type needs a service period (`RequiresServicePeriod`), that nothing
504
+ * will date, and that do not have both dates.
505
+ *
506
+ * Event lines are left out because the save stamps them from the event, and subscription lines
507
+ * because it stamps them from the term. What remains has no source but the person entering the
508
+ * order. The browser asks this before offering Confirm, so the user is told before the server's
509
+ * recognition driver refuses the booking.
510
+ *
511
+ * Reads `OrdersEngine`; the caller loads it. With the cache empty nothing counts, which leaves
512
+ * the decision to the server.
513
+ */
514
+ LinesMissingServicePeriod() {
515
+ const engine = OrdersEngine.Instance;
516
+ return this.Lines.Items.filter((line) => !(line.ServicePeriodStart && line.ServicePeriodEnd) &&
517
+ engine.ServicePeriodSource(line.ProductID) === 'Line');
432
518
  }
433
519
  /**
434
520
  * Confirm — the irreversible step.
@@ -445,9 +531,23 @@ let OrderHeaderEntity = OrderHeaderEntity_1 = class OrderHeaderEntity extends mj
445
531
  if (this.IsSaved && !this.Lines.IsLoaded) {
446
532
  await this.Lines.Load();
447
533
  }
448
- this.Status = 'Confirmed';
534
+ await this.SaveStatus('Confirmed', 'The order could not be confirmed.');
535
+ }
536
+ /**
537
+ * Move to `status` by saving, and put the previous status back if the save is refused.
538
+ *
539
+ * Without the restore a refused transition stays on the object as an unsaved edit: the screen
540
+ * reads the new status, `IsBookedOrder` hides the verbs that depend on it, and every later save
541
+ * re-sends the same transition and is refused the same way, whatever else changed.
542
+ *
543
+ * Throws with the server's reason.
544
+ */
545
+ async SaveStatus(status, fallbackMessage) {
546
+ const previous = this.Status;
547
+ this.Status = status;
449
548
  if (!(await this.Save())) {
450
- throw new Error(this.LatestResult?.CompleteMessage?.trim() || 'The order could not be confirmed.');
549
+ this.Status = previous;
550
+ throw new Error(this.LatestResult?.CompleteMessage?.trim() || fallbackMessage);
451
551
  }
452
552
  }
453
553
  /**