@autologix-engineering/shared 1.2.385 → 1.2.387
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/package.json +1 -1
- package/src/lib/data-transfer-objects/src/command/update.contract.terms.dto.d.ts +4 -1
- package/src/lib/data-transfer-objects/src/command/update.contract.terms.dto.js.map +1 -1
- package/src/lib/data-transfer-objects/src/index.d.ts +1 -0
- package/src/lib/data-transfer-objects/src/index.js +1 -0
- package/src/lib/data-transfer-objects/src/index.js.map +1 -1
- package/src/lib/data-transfer-objects/src/ltr.direct.monthly.summary.dto.d.ts +50 -0
- package/src/lib/data-transfer-objects/src/ltr.direct.monthly.summary.dto.js +166 -0
- package/src/lib/data-transfer-objects/src/ltr.direct.monthly.summary.dto.js.map +1 -0
- package/src/lib/data-transfer-objects/src/submit.ltr.payout.rows.dto.d.ts +29 -0
- package/src/lib/data-transfer-objects/src/submit.ltr.payout.rows.dto.js +11 -1
- package/src/lib/data-transfer-objects/src/submit.ltr.payout.rows.dto.js.map +1 -1
- package/src/lib/enums/src/ltr.payout.enum.d.ts +23 -5
- package/src/lib/enums/src/ltr.payout.enum.js +27 -5
- package/src/lib/enums/src/ltr.payout.enum.js.map +1 -1
- package/src/lib/models/src/client.persona.activity.log.model.d.ts +10 -1
- package/src/lib/models/src/client.persona.activity.log.model.js +6 -0
- package/src/lib/models/src/client.persona.activity.log.model.js.map +1 -1
- package/src/lib/models/src/ltr.payout.model.d.ts +22 -0
- package/src/lib/models/src/ltr.payout.model.js +20 -0
- package/src/lib/models/src/ltr.payout.model.js.map +1 -1
- package/src/lib/models/src/ltr.payout.row.group.model.d.ts +29 -0
- package/src/lib/models/src/ltr.payout.row.group.model.js +4 -0
- package/src/lib/models/src/ltr.payout.row.group.model.js.map +1 -1
- package/src/lib/models/src/numbered.invoice.model.d.ts +13 -0
- package/src/lib/models/src/numbered.invoice.model.js +1 -0
- package/src/lib/models/src/numbered.invoice.model.js.map +1 -1
- package/src/lib/utilities/src/ba.contract.extension.code.d.ts +74 -0
- package/src/lib/utilities/src/ba.contract.extension.code.js +69 -0
- package/src/lib/utilities/src/ba.contract.extension.code.js.map +1 -0
- package/src/lib/utilities/src/ba.contract.rm.refund.d.ts +59 -0
- package/src/lib/utilities/src/ba.contract.rm.refund.js +85 -0
- package/src/lib/utilities/src/ba.contract.rm.refund.js.map +1 -0
- package/src/lib/utilities/src/ba.payout.contract.selection.d.ts +79 -0
- package/src/lib/utilities/src/ba.payout.contract.selection.js +137 -0
- package/src/lib/utilities/src/ba.payout.contract.selection.js.map +1 -0
- package/src/lib/utilities/src/index.d.ts +3 -0
- package/src/lib/utilities/src/index.js +3 -0
- package/src/lib/utilities/src/index.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ltr.payout.model.js","sourceRoot":"","sources":["../../../../../../../libs/shared/src/lib/models/src/ltr.payout.model.ts"],"names":[],"mappings":"AASA,MAAM,CAAN,IAAY,cAgBX;AAhBD,WAAY,cAAc;IACzB,6CAA2B,CAAA;IAC3B,iDAA+B,CAAA;IAC/B,qCAAmB,CAAA;IACnB,yCAAuB,CAAA;IACvB,yCAAuB,CAAA;IACvB,mDAAiC,CAAA;IACjC,qDAAmC,CAAA;IACnC,uCAAqB,CAAA;IACrB,qCAAmB,CAAA;IACnB,mDAAiC,CAAA;IACjC,mCAAiB,CAAA;IACjB,iDAA+B,CAAA;IAC/B,uDAAqC,CAAA;IACrC,yDAAuC,CAAA;IACvC,+CAA6B,CAAA;AAC9B,CAAC,EAhBW,cAAc,KAAd,cAAc,QAgBzB;AAsCD,MAAM,CAAN,IAAY,
|
|
1
|
+
{"version":3,"file":"ltr.payout.model.js","sourceRoot":"","sources":["../../../../../../../libs/shared/src/lib/models/src/ltr.payout.model.ts"],"names":[],"mappings":"AASA,MAAM,CAAN,IAAY,cAgBX;AAhBD,WAAY,cAAc;IACzB,6CAA2B,CAAA;IAC3B,iDAA+B,CAAA;IAC/B,qCAAmB,CAAA;IACnB,yCAAuB,CAAA;IACvB,yCAAuB,CAAA;IACvB,mDAAiC,CAAA;IACjC,qDAAmC,CAAA;IACnC,uCAAqB,CAAA;IACrB,qCAAmB,CAAA;IACnB,mDAAiC,CAAA;IACjC,mCAAiB,CAAA;IACjB,iDAA+B,CAAA;IAC/B,uDAAqC,CAAA;IACrC,yDAAuC,CAAA;IACvC,+CAA6B,CAAA;AAC9B,CAAC,EAhBW,cAAc,KAAd,cAAc,QAgBzB;AAsCD,MAAM,CAAN,IAAY,iBA2FX;AA3FD,WAAY,iBAAiB;IAC5B,oCAAe,CAAA;IACf,iEAA4C,CAAA;IAC5C,6DAAwC,CAAA;IACxC,iDAA4B,CAAA;IAC5B,yCAAoB,CAAA;IACpB,qCAAgB,CAAA;IAChB,wCAAmB,CAAA;IACnB,kCAAa,CAAA;IACb,wCAAmB,CAAA;IACnB,sCAAiB,CAAA;IACjB,+CAA0B,CAAA;IAC1B,wCAAmB,CAAA;IACnB,2CAAsB,CAAA;IACtB,iDAA4B,CAAA;IAC5B,yDAAoC,CAAA;IACpC,iDAA4B,CAAA;IAC5B,0EAAqD,CAAA;IACrD,uEAAkD,CAAA;IAClD,2CAAsB,CAAA;IACtB,8FAAyE,CAAA;IACzE,6GAAwF,CAAA;IACxF,qDAAgC,CAAA;IAChC,mDAA8B,CAAA;IAC9B,uDAAkC,CAAA;IAClC,6CAAwB,CAAA;IACxB;;;;;;;OAOG;IACH,wDAAmC,CAAA;IACnC,4DAAuC,CAAA;IACvC,qDAAgC,CAAA;IAChC,gEAA2C,CAAA;IAC3C,oEAA+C,CAAA;IAC/C,yDAAoC,CAAA;IACpC,oEAA+C,CAAA;IAC/C,wEAAmD,CAAA;IACnD,+CAA0B,CAAA;IAC1B,iDAA4B,CAAA;IAC5B,uDAAkC,CAAA;IAClC,yDAAoC,CAAA;IACpC,qDAAgC,CAAA;IAChC,yDAAoC,CAAA;IACpC,sDAAiC,CAAA;IACjC,iDAA4B,CAAA;IAC5B,qEAAgD,CAAA;IAChD,uEAAkD,CAAA;IAClD,0DAAqC,CAAA;IACrC,0DAAqC,CAAA;IACrC,sDAAiC,CAAA;IACjC,sDAAiC,CAAA;IACjC,kDAA6B,CAAA;IAC7B,kDAA6B,CAAA;IAC7B,4DAAuC,CAAA;IACvC,4DAAuC,CAAA;IACvC,sDAAiC,CAAA;IACjC,wDAAmC,CAAA;IACnC,4CAAuB,CAAA;IACvB;;;;;;;;;OASG;IACH,kDAA6B,CAAA;IAC7B,4CAA4C;IAC5C,oDAA+B,CAAA;IAC/B,gDAA2B,CAAA;IAC3B,iEAA4C,CAAA;IAC5C,mEAA8C,CAAA;IAC9C;;;;;;;;;;OAUG;IACH,wCAAmB,CAAA;AACpB,CAAC,EA3FW,iBAAiB,KAAjB,iBAAiB,QA2F5B"}
|
|
@@ -22,6 +22,10 @@ export declare enum LTRPayoutRowGroupField {
|
|
|
22
22
|
SignedOffAt = "signedOffAt",
|
|
23
23
|
SignOffMethod = "signOffMethod",
|
|
24
24
|
SignatureImageUrl = "signatureImageUrl",
|
|
25
|
+
/** When a submitted payout superseded this entry. Null while it is live. */
|
|
26
|
+
SupersededAt = "supersededAt",
|
|
27
|
+
/** Who submitted the payout that superseded it. */
|
|
28
|
+
SupersededByRegisteredUserId = "supersededByRegisteredUserId",
|
|
25
29
|
CreatedAt = "createdAt",
|
|
26
30
|
UpdatedAt = "updatedAt"
|
|
27
31
|
}
|
|
@@ -68,6 +72,31 @@ export interface ILTRPayoutRowGroup {
|
|
|
68
72
|
[LTRPayoutRowGroupField.SignOffMethod]?: LTRPayoutSignOffMethod;
|
|
69
73
|
/** Set only when the daily sign-off captured a signature. */
|
|
70
74
|
[LTRPayoutRowGroupField.SignatureImageUrl]?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Set when a payout submitted for the booking superseded this entry.
|
|
77
|
+
*
|
|
78
|
+
* A submitted payout replaces whatever the driver logged, so from that
|
|
79
|
+
* moment the entry is no longer part of the booking's record: it stops
|
|
80
|
+
* counting toward the driver's month, stops being listed in the app, and
|
|
81
|
+
* stops appearing on the day-wise tab. Every reader filters on this, which
|
|
82
|
+
* is why the behaviour visible to a driver is the same as when these rows
|
|
83
|
+
* were deleted outright.
|
|
84
|
+
*
|
|
85
|
+
* It is a stamp rather than a DELETE because the two differ entirely in
|
|
86
|
+
* what they cost to get wrong. Superseding the wrong month is one UPDATE to
|
|
87
|
+
* undo; deleting it took the rows and their expense images with it, and
|
|
88
|
+
* nothing downstream could say afterwards what had been billed. Payout
|
|
89
|
+
* history is the thing an invoice dispute is settled from. (AD-3279)
|
|
90
|
+
*/
|
|
91
|
+
[LTRPayoutRowGroupField.SupersededAt]?: Date;
|
|
92
|
+
/**
|
|
93
|
+
* Registered user of whoever submitted the superseding payout.
|
|
94
|
+
*
|
|
95
|
+
* Taken from the authenticated request, not the body: the question this
|
|
96
|
+
* answers is who did it, and only the token knows that. Absent on entries
|
|
97
|
+
* superseded before this was recorded.
|
|
98
|
+
*/
|
|
99
|
+
[LTRPayoutRowGroupField.SupersededByRegisteredUserId]?: string;
|
|
71
100
|
[LTRPayoutRowGroupField.CreatedAt]: Date;
|
|
72
101
|
[LTRPayoutRowGroupField.UpdatedAt]: Date;
|
|
73
102
|
}
|
|
@@ -19,6 +19,10 @@ export var LTRPayoutRowGroupField;
|
|
|
19
19
|
LTRPayoutRowGroupField["SignedOffAt"] = "signedOffAt";
|
|
20
20
|
LTRPayoutRowGroupField["SignOffMethod"] = "signOffMethod";
|
|
21
21
|
LTRPayoutRowGroupField["SignatureImageUrl"] = "signatureImageUrl";
|
|
22
|
+
/** When a submitted payout superseded this entry. Null while it is live. */
|
|
23
|
+
LTRPayoutRowGroupField["SupersededAt"] = "supersededAt";
|
|
24
|
+
/** Who submitted the payout that superseded it. */
|
|
25
|
+
LTRPayoutRowGroupField["SupersededByRegisteredUserId"] = "supersededByRegisteredUserId";
|
|
22
26
|
LTRPayoutRowGroupField["CreatedAt"] = "createdAt";
|
|
23
27
|
LTRPayoutRowGroupField["UpdatedAt"] = "updatedAt";
|
|
24
28
|
})(LTRPayoutRowGroupField || (LTRPayoutRowGroupField = {}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ltr.payout.row.group.model.js","sourceRoot":"","sources":["../../../../../../../libs/shared/src/lib/models/src/ltr.payout.row.group.model.ts"],"names":[],"mappings":"AASA,MAAM,CAAN,IAAY,
|
|
1
|
+
{"version":3,"file":"ltr.payout.row.group.model.js","sourceRoot":"","sources":["../../../../../../../libs/shared/src/lib/models/src/ltr.payout.row.group.model.ts"],"names":[],"mappings":"AASA,MAAM,CAAN,IAAY,sBA0BX;AA1BD,WAAY,sBAAsB;IACjC,mDAAyB,CAAA;IACzB,2CAAiB,CAAA;IACjB,+CAAqB,CAAA;IACrB,6CAAmB,CAAA;IACnB,iDAAuB,CAAA;IACvB,2CAAiB,CAAA;IACjB,2CAAiB,CAAA;IACjB,8CAA8C;IAC9C,iDAAuB,CAAA;IACvB,6CAA6C;IAC7C,6CAAmB,CAAA;IACnB,uEAAuE;IACvE,+CAAqB,CAAA;IACrB,2CAAiB,CAAA;IACjB,yDAAyD;IACzD,iFAAuD,CAAA;IACvD,qDAA2B,CAAA;IAC3B,yDAA+B,CAAA;IAC/B,iEAAuC,CAAA;IACvC,4EAA4E;IAC5E,uDAA6B,CAAA;IAC7B,mDAAmD;IACnD,uFAA6D,CAAA;IAC7D,iDAAuB,CAAA;IACvB,iDAAuB,CAAA;AACxB,CAAC,EA1BW,sBAAsB,KAAtB,sBAAsB,QA0BjC"}
|
|
@@ -77,9 +77,21 @@ export declare enum NumberedInvoiceField {
|
|
|
77
77
|
ConsolidatedTaxInvoiceId = "consolidatedTaxInvId",
|
|
78
78
|
LTRKey = "ltrKey",
|
|
79
79
|
LTRDataKey = "ltrDataKey",
|
|
80
|
+
LTRCoverageSnapshot = "ltrCovSnap",
|
|
80
81
|
VendorTaxAddress = "vendorTaxAddress",
|
|
81
82
|
VendorTaxAddressId = "vendorTaxAddressId"
|
|
82
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The LTR vehicle master's LastInvoiceDateByGroup entries an LTR invoice moved
|
|
86
|
+
* when it was generated: `before` is each group's value going in (null when
|
|
87
|
+
* the group had never billed), `after` the value the invoice wrote. Cancelling
|
|
88
|
+
* the invoice restores `before`, so the months it billed become billable again.
|
|
89
|
+
*/
|
|
90
|
+
export interface ILTRCoverageSnapshot {
|
|
91
|
+
vehicleMasterId: string;
|
|
92
|
+
before: Record<string, string | null>;
|
|
93
|
+
after: Record<string, string>;
|
|
94
|
+
}
|
|
83
95
|
export interface INumberedInvoice extends TypedModel {
|
|
84
96
|
[NumberedInvoiceField.NumberedInvoiceId]: string;
|
|
85
97
|
[NumberedInvoiceField.InvoiceNumber]: string;
|
|
@@ -139,6 +151,7 @@ export interface INumberedInvoice extends TypedModel {
|
|
|
139
151
|
[NumberedInvoiceField.ConsolidatedTaxInvoiceId]?: string;
|
|
140
152
|
[NumberedInvoiceField.LTRKey]: string;
|
|
141
153
|
[NumberedInvoiceField.LTRDataKey]: string;
|
|
154
|
+
[NumberedInvoiceField.LTRCoverageSnapshot]?: ILTRCoverageSnapshot | null;
|
|
142
155
|
[NumberedInvoiceField.VendorTaxAddress]?: ITaxAddress;
|
|
143
156
|
[NumberedInvoiceField.VendorTaxAddressId]?: string;
|
|
144
157
|
}
|
|
@@ -61,6 +61,7 @@ export var NumberedInvoiceField;
|
|
|
61
61
|
NumberedInvoiceField["ConsolidatedTaxInvoiceId"] = "consolidatedTaxInvId";
|
|
62
62
|
NumberedInvoiceField["LTRKey"] = "ltrKey";
|
|
63
63
|
NumberedInvoiceField["LTRDataKey"] = "ltrDataKey";
|
|
64
|
+
NumberedInvoiceField["LTRCoverageSnapshot"] = "ltrCovSnap";
|
|
64
65
|
NumberedInvoiceField["VendorTaxAddress"] = "vendorTaxAddress";
|
|
65
66
|
NumberedInvoiceField["VendorTaxAddressId"] = "vendorTaxAddressId";
|
|
66
67
|
})(NumberedInvoiceField || (NumberedInvoiceField = {}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"numbered.invoice.model.js","sourceRoot":"","sources":["../../../../../../../libs/shared/src/lib/models/src/numbered.invoice.model.ts"],"names":[],"mappings":"AA0BA,MAAM,CAAN,IAAY,
|
|
1
|
+
{"version":3,"file":"numbered.invoice.model.js","sourceRoot":"","sources":["../../../../../../../libs/shared/src/lib/models/src/numbered.invoice.model.ts"],"names":[],"mappings":"AA0BA,MAAM,CAAN,IAAY,oBAuEX;AAvED,WAAY,oBAAoB;IAC/B,+DAAuC,CAAA;IACvC,uDAA+B,CAAA;IAC/B,mDAA2B,CAAA;IAC3B,8DAAsC,CAAA;IACtC,kEAA0C,CAAA;IAC1C,iDAAyB,CAAA;IACzB,qDAA6B,CAAA;IAC7B,6CAAqB,CAAA;IACrB,iDAAyB,CAAA;IACzB,oEAAoE;IACpE,0EAA0E;IAC1E,yEAAyE;IACzE,+DAAuC,CAAA;IACvC,oDAA4B,CAAA;IAC5B,wDAAgC,CAAA;IAChC,2DAAmC,CAAA;IACnC,+DAAuC,CAAA;IACvC,yCAAiB,CAAA;IACjB,uDAA+B,CAAA;IAC/B,2DAAmC,CAAA;IACnC,uDAA+B,CAAA;IAC/B,2DAAmC,CAAA;IACnC,2DAAmC,CAAA;IACnC,iDAAyB,CAAA;IACzB,iDAAyB,CAAA;IACzB,qDAA6B,CAAA;IAE7B,uDAA+B,CAAA;IAC/B,6DAAqC,CAAA;IAErC,+CAAuB,CAAA;IACvB,uCAAe,CAAA;IAEf,mDAA2B,CAAA;IAC3B,uDAA+B,CAAA;IAE/B,2DAAmC,CAAA;IACnC,+DAAuC,CAAA;IAEvC,yDAAiC,CAAA;IAEjC,0DAAkC,CAAA;IAClC,uDAA+B,CAAA;IAC/B,uDAA+B,CAAA;IAC/B,mDAA2B,CAAA;IAC3B,uDAA+B,CAAA;IAC/B,mCAAW,CAAA;IACX,qDAA6B,CAAA;IAC7B,yDAAiC,CAAA;IACjC,uEAA+C,CAAA;IAC/C,6CAAqB,CAAA;IACrB,+CAAuB,CAAA;IACvB,mDAA2B,CAAA;IAC3B,mDAA2B,CAAA;IAC3B,6CAAqB,CAAA;IACrB,yCAAiB,CAAA;IACjB,6CAAqB,CAAA;IACrB,+CAAuB,CAAA;IACvB,mDAA2B,CAAA;IAC3B,iEAAyC,CAAA;IACzC,qEAA6C,CAAA;IAC7C,gDAAwB,CAAA;IACxB,6CAAqB,CAAA;IACrB,qEAA6C,CAAA;IAC7C,yEAAiD,CAAA;IACjD,yCAAiB,CAAA;IACjB,iDAAyB,CAAA;IACzB,0DAAkC,CAAA;IAClC,6DAAqC,CAAA;IACrC,iEAAyC,CAAA;AAC1C,CAAC,EAvEW,oBAAoB,KAApB,oBAAoB,QAuE/B"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allocating the contract code for an EXTENDED BA contract — AD-3416.
|
|
3
|
+
*
|
|
4
|
+
* A BA contract's base code comes from a per-context `SeqMetadata` counter,
|
|
5
|
+
* read and incremented under a `pessimistic_write` lock
|
|
6
|
+
* (create.ba.contract.controller.ts). Every base code in a context is
|
|
7
|
+
* therefore distinct, and distinct even under concurrent creates.
|
|
8
|
+
*
|
|
9
|
+
* Extending a contract did not go anywhere near that. It derived the new code
|
|
10
|
+
* from the old one by string arithmetic alone — `1065` -> `1065E1`,
|
|
11
|
+
* `1065E1` -> `1065E2` — with no idea what already existed. And the block
|
|
12
|
+
* that would have retired the extended contract's parent is commented out in
|
|
13
|
+
* extend.ba.contract.ts, so the parent stays extendable: extend `1065`, let
|
|
14
|
+
* `1065E1` be surrendered or cancelled, extend `1065` again, and the second
|
|
15
|
+
* extension is minted as `1065E1` as well. Two distinct contract rows, one
|
|
16
|
+
* code.
|
|
17
|
+
*
|
|
18
|
+
* That matters because the BA Payout template emits one row per contract row
|
|
19
|
+
* while the upload validator, the payout calculator and the save path all key
|
|
20
|
+
* contracts by CODE. Both rows then validate against whichever duplicate the
|
|
21
|
+
* in-memory map kept last, and the application's own unedited template is
|
|
22
|
+
* rejected on upload (AD-3416; first reported as ISSUE-4 on AD-3288). On
|
|
23
|
+
* staging this had happened ten times, most recently on 25/09/2026, where
|
|
24
|
+
* extending `1073` twice produced two `1073E1` rows ninety seconds apart.
|
|
25
|
+
*
|
|
26
|
+
* `selectBAPayoutContracts` softens the symptom by collapsing records that
|
|
27
|
+
* share (BA, code, vehicle) to the most recently edited one, but that is a
|
|
28
|
+
* read-side tie-break for records keyed in twice, not a fix: applied to two
|
|
29
|
+
* genuinely different contracts it silently drops one of them from the
|
|
30
|
+
* payout. The ambiguity has to be prevented where it is created.
|
|
31
|
+
*
|
|
32
|
+
* So this module answers the allocation question instead of guessing it:
|
|
33
|
+
* given the code being extended and the codes already in use, what is the
|
|
34
|
+
* first extension suffix nobody has taken? The caller supplies the in-use set
|
|
35
|
+
* — everything in the same business context, which is the scope base codes
|
|
36
|
+
* are numbered in (on staging, no code is shared by two BAs inside one
|
|
37
|
+
* context) — and holds a lock while it does, so two simultaneous extensions
|
|
38
|
+
* cannot both read the same set.
|
|
39
|
+
*/
|
|
40
|
+
/** `1065E2` -> base `1065`, extension 2. A code with no suffix is extension 0. */
|
|
41
|
+
export interface BAContractCodeParts {
|
|
42
|
+
base: string;
|
|
43
|
+
extension: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Split a contract code into the part that identifies the contract family and
|
|
47
|
+
* the extension number.
|
|
48
|
+
*
|
|
49
|
+
* The pattern is the one the old generator used, kept deliberately: `(.+)E(\d+)`
|
|
50
|
+
* anchored at the end. Base codes are plain integers from the sequence
|
|
51
|
+
* counter, so the only `E` in a well-formed code is the extension marker.
|
|
52
|
+
*/
|
|
53
|
+
export declare function splitBAContractCode(code: unknown): BAContractCodeParts;
|
|
54
|
+
/**
|
|
55
|
+
* The first extension code of `oldContractCode` that is not already in use.
|
|
56
|
+
*
|
|
57
|
+
* `usedCodes` is every contract code already allocated in the scope the
|
|
58
|
+
* caller is protecting — for extend.ba.contract.ts, every code in the
|
|
59
|
+
* business context, whatever its status. Cancelled and Surrendered rows are
|
|
60
|
+
* deliberately included: their codes are still printed on payout statements,
|
|
61
|
+
* still stored on `BAPayoutRow.contractNo`, and still what the upload
|
|
62
|
+
* validator looks up, so reusing one recreates exactly the ambiguity this
|
|
63
|
+
* exists to stop.
|
|
64
|
+
*
|
|
65
|
+
* Codes are compared after `normalizeBAContractCode` (trim + upper), the same
|
|
66
|
+
* normalization `resolveBAPayoutContract` uses to decide that an uploaded row
|
|
67
|
+
* names a given contract. The two answers cannot disagree.
|
|
68
|
+
*
|
|
69
|
+
* Note that this does NOT guarantee the returned suffix is the old one plus
|
|
70
|
+
* one. Extending `1065` when `1065E1` is taken yields `1065E2`, which is the
|
|
71
|
+
* point: the code names a row, not a position in a chain, and nothing in the
|
|
72
|
+
* codebase parses it to find a parent (the parent is a foreign key).
|
|
73
|
+
*/
|
|
74
|
+
export declare function nextExtendedContractCode(oldContractCode: unknown, usedCodes: Iterable<unknown> | null | undefined): string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { normalizeBAContractCode } from './ba.payout.contract.selection';
|
|
2
|
+
/**
|
|
3
|
+
* Split a contract code into the part that identifies the contract family and
|
|
4
|
+
* the extension number.
|
|
5
|
+
*
|
|
6
|
+
* The pattern is the one the old generator used, kept deliberately: `(.+)E(\d+)`
|
|
7
|
+
* anchored at the end. Base codes are plain integers from the sequence
|
|
8
|
+
* counter, so the only `E` in a well-formed code is the extension marker.
|
|
9
|
+
*/
|
|
10
|
+
export function splitBAContractCode(code) {
|
|
11
|
+
const normalized = normalizeBAContractCode(code);
|
|
12
|
+
const match = normalized.match(/^(.+)E(\d+)$/);
|
|
13
|
+
if (!match)
|
|
14
|
+
return { base: normalized, extension: 0 };
|
|
15
|
+
const extension = parseInt(match[2], 10);
|
|
16
|
+
// `E0`, or a suffix so long it does not survive parseInt, is not something
|
|
17
|
+
// this codebase mints; treat the whole string as the family rather than
|
|
18
|
+
// inventing an extension number from it.
|
|
19
|
+
if (!Number.isFinite(extension) || extension < 1) {
|
|
20
|
+
return { base: normalized, extension: 0 };
|
|
21
|
+
}
|
|
22
|
+
return { base: match[1], extension };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* How far the walk will go before giving up. A context would need this many
|
|
26
|
+
* extensions of one contract to reach it; hitting it means the in-use set is
|
|
27
|
+
* wrong, and an error naming the code is far more useful than a silent
|
|
28
|
+
* duplicate or a hung request.
|
|
29
|
+
*/
|
|
30
|
+
const MAX_EXTENSION_SEARCH = 1000;
|
|
31
|
+
/**
|
|
32
|
+
* The first extension code of `oldContractCode` that is not already in use.
|
|
33
|
+
*
|
|
34
|
+
* `usedCodes` is every contract code already allocated in the scope the
|
|
35
|
+
* caller is protecting — for extend.ba.contract.ts, every code in the
|
|
36
|
+
* business context, whatever its status. Cancelled and Surrendered rows are
|
|
37
|
+
* deliberately included: their codes are still printed on payout statements,
|
|
38
|
+
* still stored on `BAPayoutRow.contractNo`, and still what the upload
|
|
39
|
+
* validator looks up, so reusing one recreates exactly the ambiguity this
|
|
40
|
+
* exists to stop.
|
|
41
|
+
*
|
|
42
|
+
* Codes are compared after `normalizeBAContractCode` (trim + upper), the same
|
|
43
|
+
* normalization `resolveBAPayoutContract` uses to decide that an uploaded row
|
|
44
|
+
* names a given contract. The two answers cannot disagree.
|
|
45
|
+
*
|
|
46
|
+
* Note that this does NOT guarantee the returned suffix is the old one plus
|
|
47
|
+
* one. Extending `1065` when `1065E1` is taken yields `1065E2`, which is the
|
|
48
|
+
* point: the code names a row, not a position in a chain, and nothing in the
|
|
49
|
+
* codebase parses it to find a parent (the parent is a foreign key).
|
|
50
|
+
*/
|
|
51
|
+
export function nextExtendedContractCode(oldContractCode, usedCodes) {
|
|
52
|
+
const { base, extension } = splitBAContractCode(oldContractCode);
|
|
53
|
+
if (!base) {
|
|
54
|
+
throw new Error('Cannot allocate an extended contract code: the contract being extended has no code');
|
|
55
|
+
}
|
|
56
|
+
const taken = new Set();
|
|
57
|
+
for (const code of usedCodes || []) {
|
|
58
|
+
const normalized = normalizeBAContractCode(code);
|
|
59
|
+
if (normalized)
|
|
60
|
+
taken.add(normalized);
|
|
61
|
+
}
|
|
62
|
+
for (let next = extension + 1; next <= extension + MAX_EXTENSION_SEARCH; next++) {
|
|
63
|
+
const candidate = `${base}E${next}`;
|
|
64
|
+
if (!taken.has(candidate))
|
|
65
|
+
return candidate;
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`Cannot allocate an extended contract code for ${base}: ${MAX_EXTENSION_SEARCH} consecutive suffixes from E${extension + 1} are already in use`);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=ba.contract.extension.code.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ba.contract.extension.code.js","sourceRoot":"","sources":["../../../../../../../libs/shared/src/lib/utilities/src/ba.contract.extension.code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAgDzE;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAa;IAChD,MAAM,UAAU,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IAC/C,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IAEtD,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,2EAA2E;IAC3E,wEAAwE;IACxE,yCAAyC;IACzC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QAClD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IAC3C,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC;AACtC,CAAC;AAED;;;;;GAKG;AACH,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAElC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,wBAAwB,CACvC,eAAwB,EACxB,SAA+C;IAE/C,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,mBAAmB,CAAC,eAAe,CAAC,CAAC;IACjE,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACd,oFAAoF,CACpF,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,SAAS,IAAI,EAAE,EAAE,CAAC;QACpC,MAAM,UAAU,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC;QACjD,IAAI,UAAU;YAAE,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACvC,CAAC;IAED,KACC,IAAI,IAAI,GAAG,SAAS,GAAG,CAAC,EACxB,IAAI,IAAI,SAAS,GAAG,oBAAoB,EACxC,IAAI,EAAE,EACL,CAAC;QACF,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC7C,CAAC;IAED,MAAM,IAAI,KAAK,CACd,iDAAiD,IAAI,KAAK,oBAAoB,+BAA+B,SAAS,GAAG,CAAC,qBAAqB,CAC/I,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { IBAContractPaidRM } from '../../models/src/index';
|
|
2
|
+
/**
|
|
3
|
+
* An RM refund cannot exceed the retention money that was collected — AD-3098
|
|
4
|
+
* QA re-open.
|
|
5
|
+
*
|
|
6
|
+
* QA set RM Status = Refunded with a refund of 10,000 on a contract that had
|
|
7
|
+
* collected 5,000, and it saved with no complaint. There was no rule anywhere:
|
|
8
|
+
* not on the form, not in the DTO, not in the controllers. The RM schedule
|
|
9
|
+
* then did the only arithmetic it can — Final = previous + Addition − Deletion
|
|
10
|
+
* — and carried −5,000 from April 2026 through to March 2027, plus the same
|
|
11
|
+
* for two other contracts (1068: 16,800 refunded against nothing collected;
|
|
12
|
+
* 1073E1: 300 against nothing collected).
|
|
13
|
+
*
|
|
14
|
+
* A negative retention balance is not a state the business has: retention
|
|
15
|
+
* money is collected before it can be given back. So the guard belongs where
|
|
16
|
+
* the number is written, not where it is displayed. The report is deliberately
|
|
17
|
+
* left honest — it keeps showing what the data says, because a report that
|
|
18
|
+
* quietly clamps a negative balance to zero hides the bad row instead of
|
|
19
|
+
* surfacing it.
|
|
20
|
+
*/
|
|
21
|
+
/** The two RM/SD statuses that mean "a refund has actually happened". */
|
|
22
|
+
export declare const BA_CONTRACT_REFUND_OPEN_STATUSES: readonly string[];
|
|
23
|
+
/**
|
|
24
|
+
* Whether a refund on an RM or SD section has actually happened.
|
|
25
|
+
*
|
|
26
|
+
* Each section is gated by its OWN status — RM by rvStatus, SD by sdStatus,
|
|
27
|
+
* never one by the other. A refund date and amount left behind by a status
|
|
28
|
+
* that has since been set back to Active would otherwise read as a refund that
|
|
29
|
+
* never occurred. The RM and SD schedules and the legacy SD export all apply
|
|
30
|
+
* this same gate, which is why it lives here and not beside one of them.
|
|
31
|
+
*/
|
|
32
|
+
export declare const isBAContractRefundOpen: (status: string | null | undefined) => boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The retention money a contract has actually collected: its non-cancelled
|
|
35
|
+
* BAContractPaidRM rows, summed.
|
|
36
|
+
*
|
|
37
|
+
* `amount` is a `decimal` column, which node-postgres hands back as a STRING.
|
|
38
|
+
* Adding those without Number() concatenates — 5000 + 5000 becomes "50005000"
|
|
39
|
+
* — so every value goes through Number() here, once. `ExtendBAContract`'s own
|
|
40
|
+
* private copy of this reduce does not, which is why callers should use this.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sumNonCancelledPaidRM(rows: IBAContractPaidRM[] | null | undefined): number;
|
|
43
|
+
/**
|
|
44
|
+
* The refund actually being recorded, or 0 when the status says none is.
|
|
45
|
+
*
|
|
46
|
+
* Mirrors what the controllers persist and what the RM schedule deducts, so
|
|
47
|
+
* the guard cannot reject a value the report would have ignored, or pass one
|
|
48
|
+
* it would have subtracted.
|
|
49
|
+
*/
|
|
50
|
+
export declare function effectiveRMRefundAmount(rvStatus: string | null | undefined, rvAmount: number | string | null | undefined): number;
|
|
51
|
+
/**
|
|
52
|
+
* `null` when the refund is within the collected RM, otherwise the message to
|
|
53
|
+
* return to the caller.
|
|
54
|
+
*
|
|
55
|
+
* Zero collected RM means zero available, so any positive refund is rejected —
|
|
56
|
+
* that is the 1068 case (16,800 refunded against nothing collected) and it is
|
|
57
|
+
* deliberate, not an edge case that slipped through.
|
|
58
|
+
*/
|
|
59
|
+
export declare function checkRMRefundWithinCollected(rvStatus: string | null | undefined, rvAmount: number | string | null | undefined, collected: number): string | null;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { BAContractPaidRMField, } from '../../models/src/index';
|
|
2
|
+
/**
|
|
3
|
+
* An RM refund cannot exceed the retention money that was collected — AD-3098
|
|
4
|
+
* QA re-open.
|
|
5
|
+
*
|
|
6
|
+
* QA set RM Status = Refunded with a refund of 10,000 on a contract that had
|
|
7
|
+
* collected 5,000, and it saved with no complaint. There was no rule anywhere:
|
|
8
|
+
* not on the form, not in the DTO, not in the controllers. The RM schedule
|
|
9
|
+
* then did the only arithmetic it can — Final = previous + Addition − Deletion
|
|
10
|
+
* — and carried −5,000 from April 2026 through to March 2027, plus the same
|
|
11
|
+
* for two other contracts (1068: 16,800 refunded against nothing collected;
|
|
12
|
+
* 1073E1: 300 against nothing collected).
|
|
13
|
+
*
|
|
14
|
+
* A negative retention balance is not a state the business has: retention
|
|
15
|
+
* money is collected before it can be given back. So the guard belongs where
|
|
16
|
+
* the number is written, not where it is displayed. The report is deliberately
|
|
17
|
+
* left honest — it keeps showing what the data says, because a report that
|
|
18
|
+
* quietly clamps a negative balance to zero hides the bad row instead of
|
|
19
|
+
* surfacing it.
|
|
20
|
+
*/
|
|
21
|
+
/** The two RM/SD statuses that mean "a refund has actually happened". */
|
|
22
|
+
export const BA_CONTRACT_REFUND_OPEN_STATUSES = [
|
|
23
|
+
'Adjusted in Car Sale',
|
|
24
|
+
'Refunded',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Whether a refund on an RM or SD section has actually happened.
|
|
28
|
+
*
|
|
29
|
+
* Each section is gated by its OWN status — RM by rvStatus, SD by sdStatus,
|
|
30
|
+
* never one by the other. A refund date and amount left behind by a status
|
|
31
|
+
* that has since been set back to Active would otherwise read as a refund that
|
|
32
|
+
* never occurred. The RM and SD schedules and the legacy SD export all apply
|
|
33
|
+
* this same gate, which is why it lives here and not beside one of them.
|
|
34
|
+
*/
|
|
35
|
+
export const isBAContractRefundOpen = (status) => BA_CONTRACT_REFUND_OPEN_STATUSES.includes((status ?? '').trim());
|
|
36
|
+
/** Money is compared at paise precision, never at float noise. */
|
|
37
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
38
|
+
/**
|
|
39
|
+
* The retention money a contract has actually collected: its non-cancelled
|
|
40
|
+
* BAContractPaidRM rows, summed.
|
|
41
|
+
*
|
|
42
|
+
* `amount` is a `decimal` column, which node-postgres hands back as a STRING.
|
|
43
|
+
* Adding those without Number() concatenates — 5000 + 5000 becomes "50005000"
|
|
44
|
+
* — so every value goes through Number() here, once. `ExtendBAContract`'s own
|
|
45
|
+
* private copy of this reduce does not, which is why callers should use this.
|
|
46
|
+
*/
|
|
47
|
+
export function sumNonCancelledPaidRM(rows) {
|
|
48
|
+
if (!rows?.length)
|
|
49
|
+
return 0;
|
|
50
|
+
return round2(rows.reduce((total, row) => row?.[BAContractPaidRMField.Cancelled]
|
|
51
|
+
? total
|
|
52
|
+
: total +
|
|
53
|
+
(Number(row?.[BAContractPaidRMField.Amount]) || 0), 0));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The refund actually being recorded, or 0 when the status says none is.
|
|
57
|
+
*
|
|
58
|
+
* Mirrors what the controllers persist and what the RM schedule deducts, so
|
|
59
|
+
* the guard cannot reject a value the report would have ignored, or pass one
|
|
60
|
+
* it would have subtracted.
|
|
61
|
+
*/
|
|
62
|
+
export function effectiveRMRefundAmount(rvStatus, rvAmount) {
|
|
63
|
+
if (!isBAContractRefundOpen(rvStatus))
|
|
64
|
+
return 0;
|
|
65
|
+
const amount = Number(rvAmount);
|
|
66
|
+
return Number.isFinite(amount) && amount > 0 ? round2(amount) : 0;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `null` when the refund is within the collected RM, otherwise the message to
|
|
70
|
+
* return to the caller.
|
|
71
|
+
*
|
|
72
|
+
* Zero collected RM means zero available, so any positive refund is rejected —
|
|
73
|
+
* that is the 1068 case (16,800 refunded against nothing collected) and it is
|
|
74
|
+
* deliberate, not an edge case that slipped through.
|
|
75
|
+
*/
|
|
76
|
+
export function checkRMRefundWithinCollected(rvStatus, rvAmount, collected) {
|
|
77
|
+
const refund = effectiveRMRefundAmount(rvStatus, rvAmount);
|
|
78
|
+
if (refund === 0)
|
|
79
|
+
return null;
|
|
80
|
+
const available = round2(Number.isFinite(collected) ? collected : 0);
|
|
81
|
+
if (refund <= available)
|
|
82
|
+
return null;
|
|
83
|
+
return `RM Refund Amount (${refund}) cannot exceed the RM collected (${available}).`;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=ba.contract.rm.refund.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ba.contract.rm.refund.js","sourceRoot":"","sources":["../../../../../../../libs/shared/src/lib/utilities/src/ba.contract.rm.refund.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,qBAAqB,GAErB,MAAM,wBAAwB,CAAC;AAEhC;;;;;;;;;;;;;;;;;;GAkBG;AAEH,yEAAyE;AACzE,MAAM,CAAC,MAAM,gCAAgC,GAAsB;IAClE,sBAAsB;IACtB,UAAU;CACV,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACrC,MAAiC,EACvB,EAAE,CAAC,gCAAgC,CAAC,QAAQ,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AAE/E,kEAAkE;AAClE,MAAM,MAAM,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;AAEhE;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CACpC,IAA4C;IAE5C,IAAI,CAAC,IAAI,EAAE,MAAM;QAAE,OAAO,CAAC,CAAC;IAC5B,OAAO,MAAM,CACZ,IAAI,CAAC,MAAM,CACV,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CACd,GAAG,EAAE,CAAC,qBAAqB,CAAC,SAAS,CAAC;QACrC,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,KAAK;YACN,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,EACrD,CAAC,CACD,CACD,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CACtC,QAAmC,EACnC,QAA4C;IAE5C,IAAI,CAAC,sBAAsB,CAAC,QAAQ,CAAC;QAAE,OAAO,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAChC,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,4BAA4B,CAC3C,QAAmC,EACnC,QAA4C,EAC5C,SAAiB;IAEjB,MAAM,MAAM,GAAG,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC3D,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE9B,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrE,IAAI,MAAM,IAAI,SAAS;QAAE,OAAO,IAAI,CAAC;IAErC,OAAO,qBAAqB,MAAM,qCAAqC,SAAS,IAAI,CAAC;AACtF,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { IBAContract } from '../../models/src/index';
|
|
2
|
+
/**
|
|
3
|
+
* Which BA contract a BA Payout row belongs to — AD-3098 QA re-open.
|
|
4
|
+
*
|
|
5
|
+
* The BA Payout template and the upload validator disagreed about contracts,
|
|
6
|
+
* and the disagreement made a whole month's payout unuploadable:
|
|
7
|
+
*
|
|
8
|
+
* • The template emitted one row per contract overlapping the period with no
|
|
9
|
+
* status filter at all, so a Cancelled contract got a row.
|
|
10
|
+
* • The validator hashed contracts into `Map<contractCode, contract>` —
|
|
11
|
+
* last write wins. `baContractCode` is NOT unique: an older extend flow
|
|
12
|
+
* minted duplicate `E<n>` codes (AD-3227, AD-3097), and a contract that is
|
|
13
|
+
* cancelled and re-raised keeps its code. On staging, `1004E1` exists twice
|
|
14
|
+
* for BA1002 — one Cancelled, one Surrendered — so the validator compared
|
|
15
|
+
* the template's Cancelled row against the Surrendered record and reported
|
|
16
|
+
* "Contract Start Date mismatch. Expected: 01/12/2025, Found: 02/05/2026".
|
|
17
|
+
* Nothing the user could type would satisfy it.
|
|
18
|
+
*
|
|
19
|
+
* The rule here is the single answer both sides now use, so they cannot drift:
|
|
20
|
+
*
|
|
21
|
+
* 1. Cancelled contracts are not payable and are dropped outright. This is
|
|
22
|
+
* the same predicate `loadReportContracts` applies to the RM and SD
|
|
23
|
+
* schedules and `findBAContractForBooking` applies to bookings, so the
|
|
24
|
+
* payout, the schedules and the booking attribution agree about which
|
|
25
|
+
* contracts exist.
|
|
26
|
+
* 2. The same contract keyed in twice — same BA, same code, same vehicle —
|
|
27
|
+
* is ONE contract, collapsed to its most recently edited record. That is
|
|
28
|
+
* the tie-break `groupDuplicateContracts` already uses in the report
|
|
29
|
+
* builders. Without it the template would print the row twice and the
|
|
30
|
+
* payout would collect the RM twice.
|
|
31
|
+
* 3. The same code on DIFFERENT vehicles stays as separate contracts (BA1000
|
|
32
|
+
* has six rows coded '1000' on six vehicles). Those are resolved by the
|
|
33
|
+
* vehicle registration carried on the uploaded row, which is the column
|
|
34
|
+
* that actually distinguishes them.
|
|
35
|
+
*/
|
|
36
|
+
export declare const normalizeBAContractCode: (raw: unknown) => string;
|
|
37
|
+
/**
|
|
38
|
+
* Registrations are compared without spaces or punctuation: the template
|
|
39
|
+
* writes the stored value verbatim, but a user who retypes a cell may enter
|
|
40
|
+
* "KA 67 BA 105" for a stored "KA67BA105", and a resolution step is the wrong
|
|
41
|
+
* place to fail over formatting. The "Vehicle mismatch" check downstream still
|
|
42
|
+
* compares the raw values, so a genuinely different vehicle is still reported.
|
|
43
|
+
*/
|
|
44
|
+
export declare const normalizeVehicleRegistration: (raw: unknown) => string;
|
|
45
|
+
/** A stored timestamp as epoch ms; 0 when it is absent or unparseable. */
|
|
46
|
+
export declare const timestampMs: (value: unknown) => number;
|
|
47
|
+
/**
|
|
48
|
+
* Most recently edited wins, falling back to most recently created.
|
|
49
|
+
*
|
|
50
|
+
* The one definition of "which of these duplicate records is the real one".
|
|
51
|
+
* `groupDuplicateContracts` in ba.contract.report.shared.ts imports it from
|
|
52
|
+
* here so the report and the payout resolve a duplicated contract to the SAME
|
|
53
|
+
* record — two answers to that question is how a sheet and a payout start
|
|
54
|
+
* disagreeing about money.
|
|
55
|
+
*/
|
|
56
|
+
export declare const isNewerRecord: (candidate: IBAContract, current: IBAContract) => boolean;
|
|
57
|
+
/** A Cancelled contract is never paid out and never appears in the template. */
|
|
58
|
+
export declare const isBAPayoutEligibleContract: (contract: IBAContract | null | undefined) => boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Drop Cancelled contracts and collapse duplicate records of the same
|
|
61
|
+
* contract, keeping the most recently edited one. Input order is preserved so
|
|
62
|
+
* the template's row order does not change for the ordinary case.
|
|
63
|
+
*
|
|
64
|
+
* A row missing any part of the identity key is never merged with anything —
|
|
65
|
+
* it gets its own slot under its own id, exactly as the report builders do.
|
|
66
|
+
*/
|
|
67
|
+
export declare function selectBAPayoutContracts(contracts: IBAContract[] | undefined | null): IBAContract[];
|
|
68
|
+
/**
|
|
69
|
+
* Pick the one contract an uploaded payout row refers to.
|
|
70
|
+
*
|
|
71
|
+
* `candidates` is every contract sharing the row's Contract ID. The vehicle
|
|
72
|
+
* registration from the row is what separates same-code contracts on different
|
|
73
|
+
* vehicles. When the row's vehicle matches none of them the newest is returned
|
|
74
|
+
* anyway, so the caller reports the real problem ("Vehicle mismatch") rather
|
|
75
|
+
* than the misleading "Contract ID not found in system".
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveBAPayoutContract(candidates: IBAContract[] | undefined | null, criteria?: {
|
|
78
|
+
vehicleRegistration?: unknown;
|
|
79
|
+
}): IBAContract | null;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { BAContractField } from '../../models/src/index';
|
|
2
|
+
import { VehicleAssociationField } from '../../models/src/vehicle.association.model';
|
|
3
|
+
import { BAContractStatus } from '../../enums/src/index';
|
|
4
|
+
/**
|
|
5
|
+
* Which BA contract a BA Payout row belongs to — AD-3098 QA re-open.
|
|
6
|
+
*
|
|
7
|
+
* The BA Payout template and the upload validator disagreed about contracts,
|
|
8
|
+
* and the disagreement made a whole month's payout unuploadable:
|
|
9
|
+
*
|
|
10
|
+
* • The template emitted one row per contract overlapping the period with no
|
|
11
|
+
* status filter at all, so a Cancelled contract got a row.
|
|
12
|
+
* • The validator hashed contracts into `Map<contractCode, contract>` —
|
|
13
|
+
* last write wins. `baContractCode` is NOT unique: an older extend flow
|
|
14
|
+
* minted duplicate `E<n>` codes (AD-3227, AD-3097), and a contract that is
|
|
15
|
+
* cancelled and re-raised keeps its code. On staging, `1004E1` exists twice
|
|
16
|
+
* for BA1002 — one Cancelled, one Surrendered — so the validator compared
|
|
17
|
+
* the template's Cancelled row against the Surrendered record and reported
|
|
18
|
+
* "Contract Start Date mismatch. Expected: 01/12/2025, Found: 02/05/2026".
|
|
19
|
+
* Nothing the user could type would satisfy it.
|
|
20
|
+
*
|
|
21
|
+
* The rule here is the single answer both sides now use, so they cannot drift:
|
|
22
|
+
*
|
|
23
|
+
* 1. Cancelled contracts are not payable and are dropped outright. This is
|
|
24
|
+
* the same predicate `loadReportContracts` applies to the RM and SD
|
|
25
|
+
* schedules and `findBAContractForBooking` applies to bookings, so the
|
|
26
|
+
* payout, the schedules and the booking attribution agree about which
|
|
27
|
+
* contracts exist.
|
|
28
|
+
* 2. The same contract keyed in twice — same BA, same code, same vehicle —
|
|
29
|
+
* is ONE contract, collapsed to its most recently edited record. That is
|
|
30
|
+
* the tie-break `groupDuplicateContracts` already uses in the report
|
|
31
|
+
* builders. Without it the template would print the row twice and the
|
|
32
|
+
* payout would collect the RM twice.
|
|
33
|
+
* 3. The same code on DIFFERENT vehicles stays as separate contracts (BA1000
|
|
34
|
+
* has six rows coded '1000' on six vehicles). Those are resolved by the
|
|
35
|
+
* vehicle registration carried on the uploaded row, which is the column
|
|
36
|
+
* that actually distinguishes them.
|
|
37
|
+
*/
|
|
38
|
+
export const normalizeBAContractCode = (raw) => raw == null ? '' : String(raw).trim().toUpperCase();
|
|
39
|
+
/**
|
|
40
|
+
* Registrations are compared without spaces or punctuation: the template
|
|
41
|
+
* writes the stored value verbatim, but a user who retypes a cell may enter
|
|
42
|
+
* "KA 67 BA 105" for a stored "KA67BA105", and a resolution step is the wrong
|
|
43
|
+
* place to fail over formatting. The "Vehicle mismatch" check downstream still
|
|
44
|
+
* compares the raw values, so a genuinely different vehicle is still reported.
|
|
45
|
+
*/
|
|
46
|
+
export const normalizeVehicleRegistration = (raw) => raw == null
|
|
47
|
+
? ''
|
|
48
|
+
: String(raw)
|
|
49
|
+
.toUpperCase()
|
|
50
|
+
.replace(/[^A-Z0-9]/g, '');
|
|
51
|
+
/** A stored timestamp as epoch ms; 0 when it is absent or unparseable. */
|
|
52
|
+
export const timestampMs = (value) => {
|
|
53
|
+
if (!value)
|
|
54
|
+
return 0;
|
|
55
|
+
const ms = new Date(value).getTime();
|
|
56
|
+
return Number.isNaN(ms) ? 0 : ms;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Most recently edited wins, falling back to most recently created.
|
|
60
|
+
*
|
|
61
|
+
* The one definition of "which of these duplicate records is the real one".
|
|
62
|
+
* `groupDuplicateContracts` in ba.contract.report.shared.ts imports it from
|
|
63
|
+
* here so the report and the payout resolve a duplicated contract to the SAME
|
|
64
|
+
* record — two answers to that question is how a sheet and a payout start
|
|
65
|
+
* disagreeing about money.
|
|
66
|
+
*/
|
|
67
|
+
export const isNewerRecord = (candidate, current) => {
|
|
68
|
+
const updatedDelta = timestampMs(candidate[BAContractField.UpdatedAt]) -
|
|
69
|
+
timestampMs(current[BAContractField.UpdatedAt]);
|
|
70
|
+
if (updatedDelta !== 0)
|
|
71
|
+
return updatedDelta > 0;
|
|
72
|
+
return (timestampMs(candidate[BAContractField.CreatedAt]) >
|
|
73
|
+
timestampMs(current[BAContractField.CreatedAt]));
|
|
74
|
+
};
|
|
75
|
+
const registrationOf = (contract) => {
|
|
76
|
+
const va = contract?.[BAContractField.VehicleAssociation];
|
|
77
|
+
return normalizeVehicleRegistration(va?.[VehicleAssociationField.Registration]);
|
|
78
|
+
};
|
|
79
|
+
/** A Cancelled contract is never paid out and never appears in the template. */
|
|
80
|
+
export const isBAPayoutEligibleContract = (contract) => !!contract &&
|
|
81
|
+
contract[BAContractField.Status] !== BAContractStatus.Cancelled;
|
|
82
|
+
/**
|
|
83
|
+
* Drop Cancelled contracts and collapse duplicate records of the same
|
|
84
|
+
* contract, keeping the most recently edited one. Input order is preserved so
|
|
85
|
+
* the template's row order does not change for the ordinary case.
|
|
86
|
+
*
|
|
87
|
+
* A row missing any part of the identity key is never merged with anything —
|
|
88
|
+
* it gets its own slot under its own id, exactly as the report builders do.
|
|
89
|
+
*/
|
|
90
|
+
export function selectBAPayoutContracts(contracts) {
|
|
91
|
+
if (!contracts?.length)
|
|
92
|
+
return [];
|
|
93
|
+
const byKey = new Map();
|
|
94
|
+
const order = [];
|
|
95
|
+
for (const contract of contracts) {
|
|
96
|
+
if (!isBAPayoutEligibleContract(contract))
|
|
97
|
+
continue;
|
|
98
|
+
const code = normalizeBAContractCode(contract[BAContractField.BAContractCode]);
|
|
99
|
+
const baId = contract[BAContractField.BusinessAssociateId] ||
|
|
100
|
+
contract[BAContractField.BusinessAssociate]?.businessAssociateId;
|
|
101
|
+
const vaId = contract[BAContractField.VehicleAssociationId];
|
|
102
|
+
const key = baId && vaId && code
|
|
103
|
+
? `${baId}|${code}|${vaId}`
|
|
104
|
+
: `id:${contract[BAContractField.BAContractId]}`;
|
|
105
|
+
const existing = byKey.get(key);
|
|
106
|
+
if (!existing) {
|
|
107
|
+
byKey.set(key, contract);
|
|
108
|
+
order.push(key);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (isNewerRecord(contract, existing))
|
|
112
|
+
byKey.set(key, contract);
|
|
113
|
+
}
|
|
114
|
+
return order.map((key) => byKey.get(key));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Pick the one contract an uploaded payout row refers to.
|
|
118
|
+
*
|
|
119
|
+
* `candidates` is every contract sharing the row's Contract ID. The vehicle
|
|
120
|
+
* registration from the row is what separates same-code contracts on different
|
|
121
|
+
* vehicles. When the row's vehicle matches none of them the newest is returned
|
|
122
|
+
* anyway, so the caller reports the real problem ("Vehicle mismatch") rather
|
|
123
|
+
* than the misleading "Contract ID not found in system".
|
|
124
|
+
*/
|
|
125
|
+
export function resolveBAPayoutContract(candidates, criteria) {
|
|
126
|
+
const eligible = selectBAPayoutContracts(candidates);
|
|
127
|
+
if (eligible.length === 0)
|
|
128
|
+
return null;
|
|
129
|
+
if (eligible.length === 1)
|
|
130
|
+
return eligible[0];
|
|
131
|
+
const wanted = normalizeVehicleRegistration(criteria?.vehicleRegistration);
|
|
132
|
+
const pool = wanted && eligible.some((c) => registrationOf(c) === wanted)
|
|
133
|
+
? eligible.filter((c) => registrationOf(c) === wanted)
|
|
134
|
+
: eligible;
|
|
135
|
+
return pool.reduce((best, c) => (isNewerRecord(c, best) ? c : best));
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=ba.payout.contract.selection.js.map
|