@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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/lib/data-transfer-objects/src/command/update.contract.terms.dto.d.ts +4 -1
  3. package/src/lib/data-transfer-objects/src/command/update.contract.terms.dto.js.map +1 -1
  4. package/src/lib/data-transfer-objects/src/index.d.ts +1 -0
  5. package/src/lib/data-transfer-objects/src/index.js +1 -0
  6. package/src/lib/data-transfer-objects/src/index.js.map +1 -1
  7. package/src/lib/data-transfer-objects/src/ltr.direct.monthly.summary.dto.d.ts +50 -0
  8. package/src/lib/data-transfer-objects/src/ltr.direct.monthly.summary.dto.js +166 -0
  9. package/src/lib/data-transfer-objects/src/ltr.direct.monthly.summary.dto.js.map +1 -0
  10. package/src/lib/data-transfer-objects/src/submit.ltr.payout.rows.dto.d.ts +29 -0
  11. package/src/lib/data-transfer-objects/src/submit.ltr.payout.rows.dto.js +11 -1
  12. package/src/lib/data-transfer-objects/src/submit.ltr.payout.rows.dto.js.map +1 -1
  13. package/src/lib/enums/src/ltr.payout.enum.d.ts +23 -5
  14. package/src/lib/enums/src/ltr.payout.enum.js +27 -5
  15. package/src/lib/enums/src/ltr.payout.enum.js.map +1 -1
  16. package/src/lib/models/src/client.persona.activity.log.model.d.ts +10 -1
  17. package/src/lib/models/src/client.persona.activity.log.model.js +6 -0
  18. package/src/lib/models/src/client.persona.activity.log.model.js.map +1 -1
  19. package/src/lib/models/src/ltr.payout.model.d.ts +22 -0
  20. package/src/lib/models/src/ltr.payout.model.js +20 -0
  21. package/src/lib/models/src/ltr.payout.model.js.map +1 -1
  22. package/src/lib/models/src/ltr.payout.row.group.model.d.ts +29 -0
  23. package/src/lib/models/src/ltr.payout.row.group.model.js +4 -0
  24. package/src/lib/models/src/ltr.payout.row.group.model.js.map +1 -1
  25. package/src/lib/models/src/numbered.invoice.model.d.ts +13 -0
  26. package/src/lib/models/src/numbered.invoice.model.js +1 -0
  27. package/src/lib/models/src/numbered.invoice.model.js.map +1 -1
  28. package/src/lib/utilities/src/ba.contract.extension.code.d.ts +74 -0
  29. package/src/lib/utilities/src/ba.contract.extension.code.js +69 -0
  30. package/src/lib/utilities/src/ba.contract.extension.code.js.map +1 -0
  31. package/src/lib/utilities/src/ba.contract.rm.refund.d.ts +59 -0
  32. package/src/lib/utilities/src/ba.contract.rm.refund.js +85 -0
  33. package/src/lib/utilities/src/ba.contract.rm.refund.js.map +1 -0
  34. package/src/lib/utilities/src/ba.payout.contract.selection.d.ts +79 -0
  35. package/src/lib/utilities/src/ba.payout.contract.selection.js +137 -0
  36. package/src/lib/utilities/src/ba.payout.contract.selection.js.map +1 -0
  37. package/src/lib/utilities/src/index.d.ts +3 -0
  38. package/src/lib/utilities/src/index.js +3 -0
  39. 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,iBAuEX;AAvED,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,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,4CAA4C;IAC5C,oDAA+B,CAAA;IAC/B,gDAA2B,CAAA;IAC3B,iEAA4C,CAAA;IAC5C,mEAA8C,CAAA;IAC9C;;;;;;;;;;OAUG;IACH,wCAAmB,CAAA;AACpB,CAAC,EAvEW,iBAAiB,KAAjB,iBAAiB,QAuE5B"}
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,sBAsBX;AAtBD,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,iDAAuB,CAAA;IACvB,iDAAuB,CAAA;AACxB,CAAC,EAtBW,sBAAsB,KAAtB,sBAAsB,QAsBjC"}
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,oBAsEX;AAtED,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,6DAAqC,CAAA;IACrC,iEAAyC,CAAA;AAC1C,CAAC,EAtEW,oBAAoB,KAApB,oBAAoB,QAsE/B"}
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