@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.
- package/dist/ExternalPaymentBehavior.d.ts +123 -0
- package/dist/ExternalPaymentBehavior.d.ts.map +1 -0
- package/dist/ExternalPaymentBehavior.js +217 -0
- package/dist/ExternalPaymentBehavior.js.map +1 -0
- package/dist/OrderHeaderEntity.d.ts +48 -2
- package/dist/OrderHeaderEntity.d.ts.map +1 -1
- package/dist/OrderHeaderEntity.js +105 -5
- package/dist/OrderHeaderEntity.js.map +1 -1
- package/dist/OrderLineEntity.d.ts +5 -0
- package/dist/OrderLineEntity.d.ts.map +1 -1
- package/dist/OrderLineEntity.js +13 -0
- package/dist/OrderLineEntity.js.map +1 -1
- package/dist/PaymentHeaderEntity.d.ts +17 -1
- package/dist/PaymentHeaderEntity.d.ts.map +1 -1
- package/dist/PaymentHeaderEntity.js +37 -1
- package/dist/PaymentHeaderEntity.js.map +1 -1
- package/dist/booked-money.d.ts +5 -1
- package/dist/booked-money.d.ts.map +1 -1
- package/dist/booked-money.js +5 -0
- package/dist/booked-money.js.map +1 -1
- package/dist/generated/entities/__mj_BizAppsOrders.d.ts +1314 -87
- package/dist/generated/entities/__mj_BizAppsOrders.d.ts.map +1 -1
- package/dist/generated/entities/__mj_BizAppsOrders.js +1916 -108
- package/dist/generated/entities/__mj_BizAppsOrders.js.map +1 -1
- package/dist/generated/remote_operations.d.ts +412 -0
- package/dist/generated/remote_operations.d.ts.map +1 -1
- package/dist/generated/remote_operations.js +126 -0
- package/dist/generated/remote_operations.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/order-address-snapshot.d.ts +52 -0
- package/dist/order-address-snapshot.d.ts.map +1 -0
- package/dist/order-address-snapshot.js +62 -0
- package/dist/order-address-snapshot.js.map +1 -0
- package/dist/pricing/OrdersEngine.d.ts +22 -2
- package/dist/pricing/OrdersEngine.d.ts.map +1 -1
- package/dist/pricing/OrdersEngine.js +45 -1
- package/dist/pricing/OrdersEngine.js.map +1 -1
- package/dist/pricing/priceOverride.d.ts +8 -0
- package/dist/pricing/priceOverride.d.ts.map +1 -1
- package/dist/pricing/priceOverride.js +12 -0
- package/dist/pricing/priceOverride.js.map +1 -1
- 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
|
-
/**
|
|
189
|
-
|
|
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;
|
|
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
|
-
/**
|
|
430
|
-
|
|
431
|
-
|
|
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.
|
|
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
|
-
|
|
549
|
+
this.Status = previous;
|
|
550
|
+
throw new Error(this.LatestResult?.CompleteMessage?.trim() || fallbackMessage);
|
|
451
551
|
}
|
|
452
552
|
}
|
|
453
553
|
/**
|