@kohost/api-client 7.7.1 → 7.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/dist/lib/bills.d.ts +74 -0
  2. package/dist/lib/bills.js +83 -0
  3. package/dist/lib/bills.js.map +1 -0
  4. package/dist/lib/deviceNotifications.js +1 -4
  5. package/dist/lib/deviceNotifications.js.map +1 -1
  6. package/dist/lib/index.d.ts +4 -0
  7. package/dist/lib/index.js +27 -2
  8. package/dist/lib/index.js.map +1 -1
  9. package/dist/lib/kfcStatsWindow.d.ts +37 -0
  10. package/dist/lib/kfcStatsWindow.js +46 -0
  11. package/dist/lib/kfcStatsWindow.js.map +1 -0
  12. package/dist/lib/lowerBoundTable.d.ts +11 -0
  13. package/dist/lib/lowerBoundTable.js +28 -0
  14. package/dist/lib/lowerBoundTable.js.map +1 -0
  15. package/dist/lib/money.d.ts +61 -0
  16. package/dist/lib/money.js +80 -0
  17. package/dist/lib/money.js.map +1 -0
  18. package/dist/lib/policy-resolvers.js +5 -4
  19. package/dist/lib/policy-resolvers.js.map +1 -1
  20. package/dist/lib/ticketRichText.d.ts +35 -0
  21. package/dist/lib/ticketRichText.js +86 -0
  22. package/dist/lib/ticketRichText.js.map +1 -0
  23. package/dist/models/bill.d.ts +17 -0
  24. package/dist/models/bill.js +37 -0
  25. package/dist/models/bill.js.map +1 -0
  26. package/dist/models/billingCategory.d.ts +17 -0
  27. package/dist/models/billingCategory.js +22 -0
  28. package/dist/models/billingCategory.js.map +1 -0
  29. package/dist/models/index.d.ts +2 -1
  30. package/dist/models/index.js +5 -3
  31. package/dist/models/index.js.map +1 -1
  32. package/dist/models/organization.js +4 -0
  33. package/dist/models/organization.js.map +1 -1
  34. package/dist/models/ticket.js +5 -0
  35. package/dist/models/ticket.js.map +1 -1
  36. package/dist/schemas/bill.d.ts +255 -0
  37. package/dist/schemas/bill.js +248 -0
  38. package/dist/schemas/bill.js.map +1 -0
  39. package/dist/schemas/billingCategory.d.ts +46 -0
  40. package/dist/schemas/billingCategory.js +38 -0
  41. package/dist/schemas/billingCategory.js.map +1 -0
  42. package/dist/schemas/definitions.d.ts +2 -2
  43. package/dist/schemas/definitions.js +3 -1
  44. package/dist/schemas/definitions.js.map +1 -1
  45. package/dist/schemas/index.d.ts +2 -1
  46. package/dist/schemas/index.js +3 -2
  47. package/dist/schemas/organization.d.ts +112 -1
  48. package/dist/schemas/organization.js +93 -0
  49. package/dist/schemas/organization.js.map +1 -1
  50. package/dist/schemas/property.d.ts +32 -0
  51. package/dist/schemas/property.js +26 -0
  52. package/dist/schemas/property.js.map +1 -1
  53. package/dist/schemas/ticket.d.ts +381 -3
  54. package/dist/schemas/ticket.js +419 -1
  55. package/dist/schemas/ticket.js.map +1 -1
  56. package/dist/useCases/acknowledgeCostDivergence.d.ts +39 -0
  57. package/dist/useCases/acknowledgeCostDivergence.js +47 -0
  58. package/dist/useCases/acknowledgeCostDivergence.js.map +1 -0
  59. package/dist/useCases/addBillLine.d.ts +36 -0
  60. package/dist/useCases/addBillLine.js +47 -0
  61. package/dist/useCases/addBillLine.js.map +1 -0
  62. package/dist/useCases/addStaffFunctionMember.d.ts +39 -0
  63. package/dist/useCases/addStaffFunctionMember.js +47 -0
  64. package/dist/useCases/addStaffFunctionMember.js.map +1 -0
  65. package/dist/useCases/cancelWorkItem.d.ts +39 -0
  66. package/dist/useCases/cancelWorkItem.js +47 -0
  67. package/dist/useCases/cancelWorkItem.js.map +1 -0
  68. package/dist/useCases/clearWorkItemNoCharge.d.ts +39 -0
  69. package/dist/useCases/clearWorkItemNoCharge.js +47 -0
  70. package/dist/useCases/clearWorkItemNoCharge.js.map +1 -0
  71. package/dist/useCases/createBill.d.ts +27 -0
  72. package/dist/useCases/createBill.js +47 -0
  73. package/dist/useCases/createBill.js.map +1 -0
  74. package/dist/useCases/createBillingCategory.d.ts +27 -0
  75. package/dist/useCases/createBillingCategory.js +47 -0
  76. package/dist/useCases/createBillingCategory.js.map +1 -0
  77. package/dist/useCases/decideApproval.d.ts +39 -0
  78. package/dist/useCases/decideApproval.js +47 -0
  79. package/dist/useCases/decideApproval.js.map +1 -0
  80. package/dist/useCases/deleteBill.d.ts +36 -0
  81. package/dist/useCases/deleteBill.js +47 -0
  82. package/dist/useCases/deleteBill.js.map +1 -0
  83. package/dist/useCases/deleteStaffUser.d.ts +36 -0
  84. package/dist/useCases/deleteStaffUser.js +47 -0
  85. package/dist/useCases/deleteStaffUser.js.map +1 -0
  86. package/dist/useCases/describeBill.d.ts +36 -0
  87. package/dist/useCases/describeBill.js +47 -0
  88. package/dist/useCases/describeBill.js.map +1 -0
  89. package/dist/useCases/describeConciergeFacilitiesViewer.d.ts +27 -0
  90. package/dist/useCases/describeConciergeFacilitiesViewer.js +47 -0
  91. package/dist/useCases/describeConciergeFacilitiesViewer.js.map +1 -0
  92. package/dist/useCases/describeConciergeStatsFilters.d.ts +27 -0
  93. package/dist/useCases/describeConciergeStatsFilters.js +47 -0
  94. package/dist/useCases/describeConciergeStatsFilters.js.map +1 -0
  95. package/dist/useCases/describeConciergeStatsHeatmap.d.ts +27 -0
  96. package/dist/useCases/describeConciergeStatsHeatmap.js +47 -0
  97. package/dist/useCases/describeConciergeStatsHeatmap.js.map +1 -0
  98. package/dist/useCases/describeConciergeStatsSummary.d.ts +27 -0
  99. package/dist/useCases/describeConciergeStatsSummary.js +47 -0
  100. package/dist/useCases/describeConciergeStatsSummary.js.map +1 -0
  101. package/dist/useCases/describeConciergeStatsTickets.d.ts +27 -0
  102. package/dist/useCases/describeConciergeStatsTickets.js +47 -0
  103. package/dist/useCases/describeConciergeStatsTickets.js.map +1 -0
  104. package/dist/useCases/describeConciergeWorkspaceFilters.d.ts +27 -0
  105. package/dist/useCases/describeConciergeWorkspaceFilters.js +47 -0
  106. package/dist/useCases/describeConciergeWorkspaceFilters.js.map +1 -0
  107. package/dist/useCases/describeOrganizationBill.d.ts +36 -0
  108. package/dist/useCases/describeOrganizationBill.js +47 -0
  109. package/dist/useCases/describeOrganizationBill.js.map +1 -0
  110. package/dist/useCases/describeSelfAccess.d.ts +27 -0
  111. package/dist/useCases/describeSelfAccess.js +47 -0
  112. package/dist/useCases/describeSelfAccess.js.map +1 -0
  113. package/dist/useCases/describeStaffFunctions.d.ts +27 -0
  114. package/dist/useCases/describeStaffFunctions.js +47 -0
  115. package/dist/useCases/describeStaffFunctions.js.map +1 -0
  116. package/dist/useCases/describeTicketStatsFilters.d.ts +27 -0
  117. package/dist/useCases/describeTicketStatsFilters.js +47 -0
  118. package/dist/useCases/describeTicketStatsFilters.js.map +1 -0
  119. package/dist/useCases/describeTicketStatsHeatmap.d.ts +27 -0
  120. package/dist/useCases/describeTicketStatsHeatmap.js +47 -0
  121. package/dist/useCases/describeTicketStatsHeatmap.js.map +1 -0
  122. package/dist/useCases/describeTicketStatsSummary.d.ts +27 -0
  123. package/dist/useCases/describeTicketStatsSummary.js +47 -0
  124. package/dist/useCases/describeTicketStatsSummary.js.map +1 -0
  125. package/dist/useCases/describeTicketStatsTickets.d.ts +27 -0
  126. package/dist/useCases/describeTicketStatsTickets.js +47 -0
  127. package/dist/useCases/describeTicketStatsTickets.js.map +1 -0
  128. package/dist/useCases/editCostEntry.d.ts +39 -0
  129. package/dist/useCases/editCostEntry.js +47 -0
  130. package/dist/useCases/editCostEntry.js.map +1 -0
  131. package/dist/useCases/index.d.ts +57 -0
  132. package/dist/useCases/index.js +116 -2
  133. package/dist/useCases/index.js.map +1 -1
  134. package/dist/useCases/listBillingCategories.d.ts +27 -0
  135. package/dist/useCases/listBillingCategories.js +47 -0
  136. package/dist/useCases/listBillingCategories.js.map +1 -0
  137. package/dist/useCases/listBills.d.ts +27 -0
  138. package/dist/useCases/listBills.js +47 -0
  139. package/dist/useCases/listBills.js.map +1 -0
  140. package/dist/useCases/listConciergeUninvoicedCosts.d.ts +27 -0
  141. package/dist/useCases/listConciergeUninvoicedCosts.js +47 -0
  142. package/dist/useCases/listConciergeUninvoicedCosts.js.map +1 -0
  143. package/dist/useCases/listConciergeWorkspaceBills.d.ts +27 -0
  144. package/dist/useCases/listConciergeWorkspaceBills.js +47 -0
  145. package/dist/useCases/listConciergeWorkspaceBills.js.map +1 -0
  146. package/dist/useCases/listConciergeWorkspaceTickets.d.ts +27 -0
  147. package/dist/useCases/listConciergeWorkspaceTickets.js +47 -0
  148. package/dist/useCases/listConciergeWorkspaceTickets.js.map +1 -0
  149. package/dist/useCases/listEligibleApprovers.d.ts +36 -0
  150. package/dist/useCases/listEligibleApprovers.js +47 -0
  151. package/dist/useCases/listEligibleApprovers.js.map +1 -0
  152. package/dist/useCases/listFacilitiesStaffIds.d.ts +27 -0
  153. package/dist/useCases/listFacilitiesStaffIds.js +47 -0
  154. package/dist/useCases/listFacilitiesStaffIds.js.map +1 -0
  155. package/dist/useCases/listKohostStaff.d.ts +27 -0
  156. package/dist/useCases/listKohostStaff.js +47 -0
  157. package/dist/useCases/listKohostStaff.js.map +1 -0
  158. package/dist/useCases/listOrganizationBills.d.ts +27 -0
  159. package/dist/useCases/listOrganizationBills.js +47 -0
  160. package/dist/useCases/listOrganizationBills.js.map +1 -0
  161. package/dist/useCases/markBillPaid.d.ts +36 -0
  162. package/dist/useCases/markBillPaid.js +47 -0
  163. package/dist/useCases/markBillPaid.js.map +1 -0
  164. package/dist/useCases/markBillSent.d.ts +36 -0
  165. package/dist/useCases/markBillSent.js +47 -0
  166. package/dist/useCases/markBillSent.js.map +1 -0
  167. package/dist/useCases/previewBill.d.ts +36 -0
  168. package/dist/useCases/previewBill.js +47 -0
  169. package/dist/useCases/previewBill.js.map +1 -0
  170. package/dist/useCases/recordCostActual.d.ts +39 -0
  171. package/dist/useCases/recordCostActual.js +47 -0
  172. package/dist/useCases/recordCostActual.js.map +1 -0
  173. package/dist/useCases/recordCostEntry.d.ts +36 -0
  174. package/dist/useCases/recordCostEntry.js +47 -0
  175. package/dist/useCases/recordCostEntry.js.map +1 -0
  176. package/dist/useCases/reinstateCostEntry.d.ts +39 -0
  177. package/dist/useCases/reinstateCostEntry.js +47 -0
  178. package/dist/useCases/reinstateCostEntry.js.map +1 -0
  179. package/dist/useCases/removeBillLine.d.ts +39 -0
  180. package/dist/useCases/removeBillLine.js +47 -0
  181. package/dist/useCases/removeBillLine.js.map +1 -0
  182. package/dist/useCases/removeStaffFunctionMember.d.ts +39 -0
  183. package/dist/useCases/removeStaffFunctionMember.js +47 -0
  184. package/dist/useCases/removeStaffFunctionMember.js.map +1 -0
  185. package/dist/useCases/requestApproval.d.ts +36 -0
  186. package/dist/useCases/requestApproval.js +47 -0
  187. package/dist/useCases/requestApproval.js.map +1 -0
  188. package/dist/useCases/rescheduleWorkItem.d.ts +39 -0
  189. package/dist/useCases/rescheduleWorkItem.js +47 -0
  190. package/dist/useCases/rescheduleWorkItem.js.map +1 -0
  191. package/dist/useCases/scheduleWorkItem.d.ts +36 -0
  192. package/dist/useCases/scheduleWorkItem.js +47 -0
  193. package/dist/useCases/scheduleWorkItem.js.map +1 -0
  194. package/dist/useCases/setCostVendorInvoice.d.ts +39 -0
  195. package/dist/useCases/setCostVendorInvoice.js +47 -0
  196. package/dist/useCases/setCostVendorInvoice.js.map +1 -0
  197. package/dist/useCases/setUserSuperAdmin.d.ts +36 -0
  198. package/dist/useCases/setUserSuperAdmin.js +47 -0
  199. package/dist/useCases/setUserSuperAdmin.js.map +1 -0
  200. package/dist/useCases/setWorkItemNoCharge.d.ts +39 -0
  201. package/dist/useCases/setWorkItemNoCharge.js +47 -0
  202. package/dist/useCases/setWorkItemNoCharge.js.map +1 -0
  203. package/dist/useCases/updateBill.d.ts +36 -0
  204. package/dist/useCases/updateBill.js +47 -0
  205. package/dist/useCases/updateBill.js.map +1 -0
  206. package/dist/useCases/updateBillLine.d.ts +39 -0
  207. package/dist/useCases/updateBillLine.js +47 -0
  208. package/dist/useCases/updateBillLine.js.map +1 -0
  209. package/dist/useCases/updateBillingCategory.d.ts +36 -0
  210. package/dist/useCases/updateBillingCategory.js +47 -0
  211. package/dist/useCases/updateBillingCategory.js.map +1 -0
  212. package/dist/useCases/updateOrganizationBillingAddress.d.ts +36 -0
  213. package/dist/useCases/updateOrganizationBillingAddress.js +47 -0
  214. package/dist/useCases/updateOrganizationBillingAddress.js.map +1 -0
  215. package/dist/useCases/updateOrganizationBillingContacts.d.ts +36 -0
  216. package/dist/useCases/updateOrganizationBillingContacts.js +47 -0
  217. package/dist/useCases/updateOrganizationBillingContacts.js.map +1 -0
  218. package/dist/useCases/updateOrganizationTaxExempt.d.ts +36 -0
  219. package/dist/useCases/updateOrganizationTaxExempt.js +47 -0
  220. package/dist/useCases/updateOrganizationTaxExempt.js.map +1 -0
  221. package/dist/useCases/updateStaffUser.d.ts +36 -0
  222. package/dist/useCases/updateStaffUser.js +47 -0
  223. package/dist/useCases/updateStaffUser.js.map +1 -0
  224. package/dist/useCases/voidBill.d.ts +36 -0
  225. package/dist/useCases/voidBill.js +47 -0
  226. package/dist/useCases/voidBill.js.map +1 -0
  227. package/dist/useCases/writeOffCostEntry.d.ts +39 -0
  228. package/dist/useCases/writeOffCostEntry.js +47 -0
  229. package/dist/useCases/writeOffCostEntry.js.map +1 -0
  230. package/dist/validate.d.ts +25 -8
  231. package/dist/validate.js +9 -4
  232. package/dist/validate.js.map +1 -1
  233. package/package.json +6 -6
  234. package/dist/models/order.d.ts +0 -17
  235. package/dist/models/order.js +0 -33
  236. package/dist/models/order.js.map +0 -1
  237. package/dist/schemas/order.d.ts +0 -186
  238. package/dist/schemas/order.js +0 -157
  239. package/dist/schemas/order.js.map +0 -1
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The bill rules the API and the app must apply identically: the rate a bill
3
+ * taxes at, its grand total, and the date it reads as effective. The API's send
4
+ * gate, its PDF render and its cross-org sort key all derive from these, and so
5
+ * does every total the browser prints — the ledger row, the org bill list, the
6
+ * detail view. Written once here so a total shown next to a sort order the
7
+ * server produced cannot disagree with it.
8
+ *
9
+ * Structural types carry just the fields a rule reads, so schema-derived
10
+ * documents assign without adapters.
11
+ */
12
+ export type BillTaxFields = {
13
+ taxRate?: number | null;
14
+ shippingAmount?: number | null;
15
+ };
16
+ /**
17
+ * The rate a bill actually taxes at: its own explicit rate, else the platform
18
+ * default while drafting. An exempt org always resolves to zero.
19
+ */
20
+ export declare function effectiveTaxRate(bill: BillTaxFields, taxExempt: boolean): number;
21
+ export type TaxableLine = {
22
+ amount?: number | null;
23
+ taxable?: boolean;
24
+ };
25
+ /** The sum of the taxable lines' amounts — credits included — in integer cents. */
26
+ export declare function taxableSubtotal(lines: readonly TaxableLine[]): number;
27
+ /**
28
+ * Subtotal, tax, shipping, and grand total for one bill, from already-resolved
29
+ * line amounts — the single derivation the document, the send gate, and the org
30
+ * projection all share.
31
+ */
32
+ export declare function billTotals(lines: readonly TaxableLine[], bill: BillTaxFields, taxExempt: boolean): {
33
+ subtotal: number;
34
+ taxRate: number;
35
+ taxAmount: number;
36
+ shippingAmount: number;
37
+ total: number;
38
+ };
39
+ /**
40
+ * The exemption a bill's totals resolve with: while drafting it live-reads
41
+ * from the org; once sent the bill carries its own snapshot, frozen at
42
+ * mark-sent. The one resolution the document render, the detail read, and the
43
+ * ledger all share, so their totals cannot drift.
44
+ */
45
+ export declare function resolveBillTaxExempt(bill: {
46
+ status: string;
47
+ taxExempt?: boolean | null;
48
+ }, orgTaxExempt: boolean): boolean;
49
+ export type SettledBillTotalSource = BillTaxFields & {
50
+ status: string;
51
+ taxExempt?: boolean | null;
52
+ lines?: readonly TaxableLine[] | null;
53
+ };
54
+ /**
55
+ * A bill's grand total as a summary row shows it, from the snapshot the bill
56
+ * itself carries. Null for a draft — draft cost lines hold no amount until
57
+ * mark-sent resolves and freezes them, so a draft has no honest total to show
58
+ * and no org read is reached for. A settled (sent/paid/void) bill's frozen
59
+ * lines, rate, and exemption yield exactly the total the detail document
60
+ * prints.
61
+ */
62
+ export declare function settledBillTotal(bill: SettledBillTotalSource): number | null;
63
+ export type BillEffectiveDateSource = {
64
+ sentAt?: Date | string | null;
65
+ createdAt?: Date | string | null;
66
+ };
67
+ /**
68
+ * The date a bill reads as: when it was sent once it has been, else when it was
69
+ * started. This is the key the cross-org ledger sorts, filters and pages on, so
70
+ * the server evaluates the same rule in Mongo (`$ifNull: ["$sentAt",
71
+ * "$createdAt"]`) to order a page. Returned as an ISO string; a bill with
72
+ * neither date falls to the epoch rather than to an unsortable null.
73
+ */
74
+ export declare function billEffectiveDate(bill: BillEffectiveDateSource): string;
@@ -0,0 +1,83 @@
1
+ import { billGrandTotal, billTaxAmount, billTotal } from "./money.js";
2
+ //#region .generated/lib/bills.ts
3
+ /**
4
+ * The bill rules the API and the app must apply identically: the rate a bill
5
+ * taxes at, its grand total, and the date it reads as effective. The API's send
6
+ * gate, its PDF render and its cross-org sort key all derive from these, and so
7
+ * does every total the browser prints — the ledger row, the org bill list, the
8
+ * detail view. Written once here so a total shown next to a sort order the
9
+ * server produced cannot disagree with it.
10
+ *
11
+ * Structural types carry just the fields a rule reads, so schema-derived
12
+ * documents assign without adapters.
13
+ */
14
+ /**
15
+ * The rate a bill actually taxes at: its own explicit rate, else the platform
16
+ * default while drafting. An exempt org always resolves to zero.
17
+ */
18
+ function effectiveTaxRate(bill, taxExempt) {
19
+ if (taxExempt) return 0;
20
+ return bill.taxRate ?? 8.375;
21
+ }
22
+ /** The sum of the taxable lines' amounts — credits included — in integer cents. */
23
+ function taxableSubtotal(lines) {
24
+ return billTotal(lines.filter((line) => line.taxable).map((line) => line.amount ?? 0));
25
+ }
26
+ /**
27
+ * Subtotal, tax, shipping, and grand total for one bill, from already-resolved
28
+ * line amounts — the single derivation the document, the send gate, and the org
29
+ * projection all share.
30
+ */
31
+ function billTotals(lines, bill, taxExempt) {
32
+ const subtotal = billTotal(lines.map((line) => line.amount ?? 0));
33
+ const taxRate = effectiveTaxRate(bill, taxExempt);
34
+ const taxAmount = billTaxAmount(taxableSubtotal(lines), taxRate);
35
+ const shippingAmount = bill.shippingAmount ?? 0;
36
+ return {
37
+ subtotal,
38
+ taxRate,
39
+ taxAmount,
40
+ shippingAmount,
41
+ total: billGrandTotal({
42
+ subtotal,
43
+ tax: taxAmount,
44
+ shipping: shippingAmount
45
+ })
46
+ };
47
+ }
48
+ /**
49
+ * The exemption a bill's totals resolve with: while drafting it live-reads
50
+ * from the org; once sent the bill carries its own snapshot, frozen at
51
+ * mark-sent. The one resolution the document render, the detail read, and the
52
+ * ledger all share, so their totals cannot drift.
53
+ */
54
+ function resolveBillTaxExempt(bill, orgTaxExempt) {
55
+ return bill.status === "draft" ? orgTaxExempt : bill.taxExempt ?? false;
56
+ }
57
+ /**
58
+ * A bill's grand total as a summary row shows it, from the snapshot the bill
59
+ * itself carries. Null for a draft — draft cost lines hold no amount until
60
+ * mark-sent resolves and freezes them, so a draft has no honest total to show
61
+ * and no org read is reached for. A settled (sent/paid/void) bill's frozen
62
+ * lines, rate, and exemption yield exactly the total the detail document
63
+ * prints.
64
+ */
65
+ function settledBillTotal(bill) {
66
+ if (bill.status === "draft") return null;
67
+ return billTotals(bill.lines ?? [], bill, resolveBillTaxExempt(bill, false)).total;
68
+ }
69
+ /**
70
+ * The date a bill reads as: when it was sent once it has been, else when it was
71
+ * started. This is the key the cross-org ledger sorts, filters and pages on, so
72
+ * the server evaluates the same rule in Mongo (`$ifNull: ["$sentAt",
73
+ * "$createdAt"]`) to order a page. Returned as an ISO string; a bill with
74
+ * neither date falls to the epoch rather than to an unsortable null.
75
+ */
76
+ function billEffectiveDate(bill) {
77
+ const value = bill.sentAt ?? bill.createdAt ?? 0;
78
+ return new Date(value).toISOString();
79
+ }
80
+ //#endregion
81
+ export { billEffectiveDate, billTotals, effectiveTaxRate, resolveBillTaxExempt, settledBillTotal, taxableSubtotal };
82
+
83
+ //# sourceMappingURL=bills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bills.js","names":[],"sources":["../../.generated/lib/bills.ts"],"sourcesContent":["/**\n * The bill rules the API and the app must apply identically: the rate a bill\n * taxes at, its grand total, and the date it reads as effective. The API's send\n * gate, its PDF render and its cross-org sort key all derive from these, and so\n * does every total the browser prints — the ledger row, the org bill list, the\n * detail view. Written once here so a total shown next to a sort order the\n * server produced cannot disagree with it.\n *\n * Structural types carry just the fields a rule reads, so schema-derived\n * documents assign without adapters.\n */\n\nimport {\n DEFAULT_TAX_RATE,\n billGrandTotal,\n billTaxAmount,\n billTotal,\n} from \"./money.js\";\n\nexport type BillTaxFields = {\n taxRate?: number | null;\n shippingAmount?: number | null;\n};\n\n/**\n * The rate a bill actually taxes at: its own explicit rate, else the platform\n * default while drafting. An exempt org always resolves to zero.\n */\nexport function effectiveTaxRate(\n bill: BillTaxFields,\n taxExempt: boolean,\n): number {\n if (taxExempt) return 0;\n return bill.taxRate ?? DEFAULT_TAX_RATE;\n}\n\nexport type TaxableLine = { amount?: number | null; taxable?: boolean };\n\n/** The sum of the taxable lines' amounts — credits included — in integer cents. */\nexport function taxableSubtotal(lines: readonly TaxableLine[]): number {\n return billTotal(\n lines.filter((line) => line.taxable).map((line) => line.amount ?? 0),\n );\n}\n\n/**\n * Subtotal, tax, shipping, and grand total for one bill, from already-resolved\n * line amounts — the single derivation the document, the send gate, and the org\n * projection all share.\n */\nexport function billTotals(\n lines: readonly TaxableLine[],\n bill: BillTaxFields,\n taxExempt: boolean,\n): {\n subtotal: number;\n taxRate: number;\n taxAmount: number;\n shippingAmount: number;\n total: number;\n} {\n const subtotal = billTotal(lines.map((line) => line.amount ?? 0));\n const taxRate = effectiveTaxRate(bill, taxExempt);\n const taxAmount = billTaxAmount(taxableSubtotal(lines), taxRate);\n const shippingAmount = bill.shippingAmount ?? 0;\n return {\n subtotal,\n taxRate,\n taxAmount,\n shippingAmount,\n total: billGrandTotal({\n subtotal,\n tax: taxAmount,\n shipping: shippingAmount,\n }),\n };\n}\n\n/**\n * The exemption a bill's totals resolve with: while drafting it live-reads\n * from the org; once sent the bill carries its own snapshot, frozen at\n * mark-sent. The one resolution the document render, the detail read, and the\n * ledger all share, so their totals cannot drift.\n */\nexport function resolveBillTaxExempt(\n bill: { status: string; taxExempt?: boolean | null },\n orgTaxExempt: boolean,\n): boolean {\n return bill.status === \"draft\" ? orgTaxExempt : (bill.taxExempt ?? false);\n}\n\nexport type SettledBillTotalSource = BillTaxFields & {\n status: string;\n taxExempt?: boolean | null;\n lines?: readonly TaxableLine[] | null;\n};\n\n/**\n * A bill's grand total as a summary row shows it, from the snapshot the bill\n * itself carries. Null for a draft — draft cost lines hold no amount until\n * mark-sent resolves and freezes them, so a draft has no honest total to show\n * and no org read is reached for. A settled (sent/paid/void) bill's frozen\n * lines, rate, and exemption yield exactly the total the detail document\n * prints.\n */\nexport function settledBillTotal(bill: SettledBillTotalSource): number | null {\n if (bill.status === \"draft\") return null;\n return billTotals(bill.lines ?? [], bill, resolveBillTaxExempt(bill, false))\n .total;\n}\n\nexport type BillEffectiveDateSource = {\n sentAt?: Date | string | null;\n createdAt?: Date | string | null;\n};\n\n/**\n * The date a bill reads as: when it was sent once it has been, else when it was\n * started. This is the key the cross-org ledger sorts, filters and pages on, so\n * the server evaluates the same rule in Mongo (`$ifNull: [\"$sentAt\",\n * \"$createdAt\"]`) to order a page. Returned as an ISO string; a bill with\n * neither date falls to the epoch rather than to an unsortable null.\n */\nexport function billEffectiveDate(bill: BillEffectiveDateSource): string {\n const value = bill.sentAt ?? bill.createdAt ?? 0;\n return new Date(value).toISOString();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA4BA,SAAgB,iBACd,MACA,WACQ;CACR,IAAI,WAAW,OAAO;CACtB,OAAO,KAAK,WAAA;AACd;;AAKA,SAAgB,gBAAgB,OAAuC;CACrE,OAAO,UACL,MAAM,QAAQ,SAAS,KAAK,OAAO,CAAC,CAAC,KAAK,SAAS,KAAK,UAAU,CAAC,CACrE;AACF;;;;;;AAOA,SAAgB,WACd,OACA,MACA,WAOA;CACA,MAAM,WAAW,UAAU,MAAM,KAAK,SAAS,KAAK,UAAU,CAAC,CAAC;CAChE,MAAM,UAAU,iBAAiB,MAAM,SAAS;CAChD,MAAM,YAAY,cAAc,gBAAgB,KAAK,GAAG,OAAO;CAC/D,MAAM,iBAAiB,KAAK,kBAAkB;CAC9C,OAAO;EACL;EACA;EACA;EACA;EACA,OAAO,eAAe;GACpB;GACA,KAAK;GACL,UAAU;EACZ,CAAC;CACH;AACF;;;;;;;AAQA,SAAgB,qBACd,MACA,cACS;CACT,OAAO,KAAK,WAAW,UAAU,eAAgB,KAAK,aAAa;AACrE;;;;;;;;;AAgBA,SAAgB,iBAAiB,MAA6C;CAC5E,IAAI,KAAK,WAAW,SAAS,OAAO;CACpC,OAAO,WAAW,KAAK,SAAS,CAAC,GAAG,MAAM,qBAAqB,MAAM,KAAK,CAAC,CAAC,CACzE;AACL;;;;;;;;AAcA,SAAgB,kBAAkB,MAAuC;CACvE,MAAM,QAAQ,KAAK,UAAU,KAAK,aAAa;CAC/C,OAAO,IAAI,KAAK,KAAK,CAAC,CAAC,YAAY;AACrC"}
@@ -22,10 +22,7 @@ function inferDeviceNotifications(device) {
22
22
  if ("powerLevel" in device) set.add("powerTrouble");
23
23
  if ("equipment" in device) set.add("hardwareFailure");
24
24
  switch (device.type) {
25
- case "thermostat":
26
- for (const n of THERMOSTAT_INFERRED) set.add(n);
27
- break;
28
- default: break;
25
+ case "thermostat": for (const n of THERMOSTAT_INFERRED) set.add(n);
29
26
  }
30
27
  return Array.from(set);
31
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"deviceNotifications.js","names":[],"sources":["../../.generated/lib/deviceNotifications.ts"],"sourcesContent":["export type DeviceNotificationName =\n | \"communicationFailure\"\n | \"replaceBattery\"\n | \"hardwareFailure\"\n | \"airFilterNeedsCleaned\"\n | \"airFilterNeedsReplaced\"\n | \"scheduleMaintenance\"\n | \"outsideSafeTemperatureRange\"\n | \"outsideSafeHumidityRange\"\n | \"bellTrouble\"\n | \"monitoringTrouble\"\n | \"powerTrouble\"\n | \"burglarAlarm\"\n | \"fireAlarm\"\n | \"smokeDetected\"\n | \"doorAjar\"\n | \"configIssue\"\n | \"acMainsDisconnected\";\n\nexport interface InferDeviceNotificationsInput {\n type?: string;\n discriminator?: string;\n offline?: unknown;\n batteryLevel?: unknown;\n bellTrouble?: unknown;\n monitoringTrouble?: unknown;\n powerLevel?: unknown;\n equipment?: unknown;\n}\n\nconst THERMOSTAT_INFERRED: DeviceNotificationName[] = [\n \"airFilterNeedsCleaned\",\n \"airFilterNeedsReplaced\",\n \"scheduleMaintenance\",\n \"outsideSafeTemperatureRange\",\n \"outsideSafeHumidityRange\",\n \"hardwareFailure\",\n];\n\n/**\n * Returns the canonical notifications a device can emit, derived from its\n * shape. Replaces the legacy `device.supportedNotifications[]` reads — driver\n * implementations no longer need to declare this list.\n */\nexport function inferDeviceNotifications(\n device: InferDeviceNotificationsInput | null | undefined,\n): DeviceNotificationName[] {\n if (!device || !device.type) return [];\n const set = new Set<DeviceNotificationName>();\n\n if (\"offline\" in device) set.add(\"communicationFailure\");\n\n if (\"batteryLevel\" in device && device.batteryLevel != null) {\n set.add(\"replaceBattery\");\n }\n\n if (\"bellTrouble\" in device) set.add(\"bellTrouble\");\n if (\"monitoringTrouble\" in device) set.add(\"monitoringTrouble\");\n if (\"powerLevel\" in device) set.add(\"powerTrouble\");\n\n if (\"equipment\" in device) set.add(\"hardwareFailure\");\n\n switch (device.type) {\n case \"thermostat\":\n for (const n of THERMOSTAT_INFERRED) set.add(n);\n break;\n default:\n break;\n }\n\n return Array.from(set);\n}\n"],"mappings":";AA8BA,MAAM,sBAAgD;CACpD;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;AAOA,SAAgB,yBACd,QAC0B;CAC1B,IAAI,CAAC,UAAU,CAAC,OAAO,MAAM,OAAO,CAAC;CACrC,MAAM,sBAAM,IAAI,IAA4B;CAE5C,IAAI,aAAa,QAAQ,IAAI,IAAI,sBAAsB;CAEvD,IAAI,kBAAkB,UAAU,OAAO,gBAAgB,MACrD,IAAI,IAAI,gBAAgB;CAG1B,IAAI,iBAAiB,QAAQ,IAAI,IAAI,aAAa;CAClD,IAAI,uBAAuB,QAAQ,IAAI,IAAI,mBAAmB;CAC9D,IAAI,gBAAgB,QAAQ,IAAI,IAAI,cAAc;CAElD,IAAI,eAAe,QAAQ,IAAI,IAAI,iBAAiB;CAEpD,QAAQ,OAAO,MAAf;EACE,KAAK;GACH,KAAK,MAAM,KAAK,qBAAqB,IAAI,IAAI,CAAC;GAC9C;EACF,SACE;CACJ;CAEA,OAAO,MAAM,KAAK,GAAG;AACvB"}
1
+ {"version":3,"file":"deviceNotifications.js","names":[],"sources":["../../.generated/lib/deviceNotifications.ts"],"sourcesContent":["export type DeviceNotificationName =\n | \"communicationFailure\"\n | \"replaceBattery\"\n | \"hardwareFailure\"\n | \"airFilterNeedsCleaned\"\n | \"airFilterNeedsReplaced\"\n | \"scheduleMaintenance\"\n | \"outsideSafeTemperatureRange\"\n | \"outsideSafeHumidityRange\"\n | \"bellTrouble\"\n | \"monitoringTrouble\"\n | \"powerTrouble\"\n | \"burglarAlarm\"\n | \"fireAlarm\"\n | \"smokeDetected\"\n | \"doorAjar\"\n | \"configIssue\"\n | \"acMainsDisconnected\";\n\nexport interface InferDeviceNotificationsInput {\n type?: string;\n discriminator?: string;\n offline?: unknown;\n batteryLevel?: unknown;\n bellTrouble?: unknown;\n monitoringTrouble?: unknown;\n powerLevel?: unknown;\n equipment?: unknown;\n}\n\nconst THERMOSTAT_INFERRED: DeviceNotificationName[] = [\n \"airFilterNeedsCleaned\",\n \"airFilterNeedsReplaced\",\n \"scheduleMaintenance\",\n \"outsideSafeTemperatureRange\",\n \"outsideSafeHumidityRange\",\n \"hardwareFailure\",\n];\n\n/**\n * Returns the canonical notifications a device can emit, derived from its\n * shape. Replaces the legacy `device.supportedNotifications[]` reads — driver\n * implementations no longer need to declare this list.\n */\nexport function inferDeviceNotifications(\n device: InferDeviceNotificationsInput | null | undefined,\n): DeviceNotificationName[] {\n if (!device || !device.type) return [];\n const set = new Set<DeviceNotificationName>();\n\n if (\"offline\" in device) set.add(\"communicationFailure\");\n\n if (\"batteryLevel\" in device && device.batteryLevel != null) {\n set.add(\"replaceBattery\");\n }\n\n if (\"bellTrouble\" in device) set.add(\"bellTrouble\");\n if (\"monitoringTrouble\" in device) set.add(\"monitoringTrouble\");\n if (\"powerLevel\" in device) set.add(\"powerTrouble\");\n\n if (\"equipment\" in device) set.add(\"hardwareFailure\");\n\n switch (device.type) {\n case \"thermostat\":\n for (const n of THERMOSTAT_INFERRED) set.add(n);\n break;\n default:\n break;\n }\n\n return Array.from(set);\n}\n"],"mappings":";AA8BA,MAAM,sBAAgD;CACpD;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;AAOA,SAAgB,yBACd,QAC0B;CAC1B,IAAI,CAAC,UAAU,CAAC,OAAO,MAAM,OAAO,CAAC;CACrC,MAAM,sBAAM,IAAI,IAA4B;CAE5C,IAAI,aAAa,QAAQ,IAAI,IAAI,sBAAsB;CAEvD,IAAI,kBAAkB,UAAU,OAAO,gBAAgB,MACrD,IAAI,IAAI,gBAAgB;CAG1B,IAAI,iBAAiB,QAAQ,IAAI,IAAI,aAAa;CAClD,IAAI,uBAAuB,QAAQ,IAAI,IAAI,mBAAmB;CAC9D,IAAI,gBAAgB,QAAQ,IAAI,IAAI,cAAc;CAElD,IAAI,eAAe,QAAQ,IAAI,IAAI,iBAAiB;CAEpD,QAAQ,OAAO,MAAf;EACE,KAAK,cACH,KAAK,MAAM,KAAK,qBAAqB,IAAI,IAAI,CAAC;CAIlD;CAEA,OAAO,MAAM,KAAK,GAAG;AACvB"}
@@ -2,3 +2,7 @@ export { inferDeviceNotifications, type DeviceNotificationName, type InferDevice
2
2
  export { categoryForDevice, SYSTEM_CATEGORIES, SYSTEM_CATEGORY_LABELS, type SystemCategory, type CategoryForDeviceInput, } from "./systemCategories.js";
3
3
  export { getAllowedResourceIds, getAllowedDeviceTypes, getAllowedFeatures, type PolicyLike, type PolicyPermissionLike, } from "./policy-resolvers.js";
4
4
  export { MAX_MESSAGE_ATTACHMENTS } from "./messageAttachments.js";
5
+ export { DEFAULT_MARKUP_TIERS, resolveMarkupTierTable, keyedMarkupPercent, DEFAULT_TAX_RATE, derivePrice, billTotal, billTaxAmount, billGrandTotal, formatMoney, } from "./money.js";
6
+ export { effectiveTaxRate, taxableSubtotal, billTotals, resolveBillTaxExempt, settledBillTotal, billEffectiveDate, type BillTaxFields, type TaxableLine, type SettledBillTotalSource, type BillEffectiveDateSource, } from "./bills.js";
7
+ export { KFC_STATS_TIMEZONE, KFC_STATS_DEFAULT_WINDOW_DAYS, KFC_STATS_MAX_RANGE_DAYS, resolveKfcStatsWindowBounds, type KfcStatsWindowBounds, } from "./kfcStatsWindow.js";
8
+ export { serializeRichTextBody, isWellFormedRichText, type RichTextContent, type RichTextBlockNode, type RichTextInlineNode, type RichTextListItemNode, type RichTextMentionNode, } from "./ticketRichText.js";
package/dist/lib/index.js CHANGED
@@ -3,18 +3,43 @@ import { inferDeviceNotifications } from "./deviceNotifications.js";
3
3
  import { SYSTEM_CATEGORIES, SYSTEM_CATEGORY_LABELS, categoryForDevice } from "./systemCategories.js";
4
4
  import { getAllowedDeviceTypes, getAllowedFeatures, getAllowedResourceIds } from "./policy-resolvers.js";
5
5
  import { MAX_MESSAGE_ATTACHMENTS } from "./messageAttachments.js";
6
+ import { DEFAULT_MARKUP_TIERS, DEFAULT_TAX_RATE, billGrandTotal, billTaxAmount, billTotal, derivePrice, formatMoney, keyedMarkupPercent, resolveMarkupTierTable } from "./money.js";
7
+ import { billEffectiveDate, billTotals, effectiveTaxRate, resolveBillTaxExempt, settledBillTotal, taxableSubtotal } from "./bills.js";
8
+ import { KFC_STATS_DEFAULT_WINDOW_DAYS, KFC_STATS_MAX_RANGE_DAYS, KFC_STATS_TIMEZONE, resolveKfcStatsWindowBounds } from "./kfcStatsWindow.js";
9
+ import { isWellFormedRichText, serializeRichTextBody } from "./ticketRichText.js";
6
10
  //#region .generated/lib/index.ts
7
11
  var lib_exports = /* @__PURE__ */ __exportAll({
12
+ DEFAULT_MARKUP_TIERS: () => DEFAULT_MARKUP_TIERS,
13
+ DEFAULT_TAX_RATE: () => DEFAULT_TAX_RATE,
14
+ KFC_STATS_DEFAULT_WINDOW_DAYS: () => 30,
15
+ KFC_STATS_MAX_RANGE_DAYS: () => 366,
16
+ KFC_STATS_TIMEZONE: () => KFC_STATS_TIMEZONE,
8
17
  MAX_MESSAGE_ATTACHMENTS: () => 15,
9
18
  SYSTEM_CATEGORIES: () => SYSTEM_CATEGORIES,
10
19
  SYSTEM_CATEGORY_LABELS: () => SYSTEM_CATEGORY_LABELS,
20
+ billEffectiveDate: () => billEffectiveDate,
21
+ billGrandTotal: () => billGrandTotal,
22
+ billTaxAmount: () => billTaxAmount,
23
+ billTotal: () => billTotal,
24
+ billTotals: () => billTotals,
11
25
  categoryForDevice: () => categoryForDevice,
26
+ derivePrice: () => derivePrice,
27
+ effectiveTaxRate: () => effectiveTaxRate,
28
+ formatMoney: () => formatMoney,
12
29
  getAllowedDeviceTypes: () => getAllowedDeviceTypes,
13
30
  getAllowedFeatures: () => getAllowedFeatures,
14
31
  getAllowedResourceIds: () => getAllowedResourceIds,
15
- inferDeviceNotifications: () => inferDeviceNotifications
32
+ inferDeviceNotifications: () => inferDeviceNotifications,
33
+ isWellFormedRichText: () => isWellFormedRichText,
34
+ keyedMarkupPercent: () => keyedMarkupPercent,
35
+ resolveBillTaxExempt: () => resolveBillTaxExempt,
36
+ resolveKfcStatsWindowBounds: () => resolveKfcStatsWindowBounds,
37
+ resolveMarkupTierTable: () => resolveMarkupTierTable,
38
+ serializeRichTextBody: () => serializeRichTextBody,
39
+ settledBillTotal: () => settledBillTotal,
40
+ taxableSubtotal: () => taxableSubtotal
16
41
  });
17
42
  //#endregion
18
- export { MAX_MESSAGE_ATTACHMENTS, SYSTEM_CATEGORIES, SYSTEM_CATEGORY_LABELS, categoryForDevice, getAllowedDeviceTypes, getAllowedFeatures, getAllowedResourceIds, inferDeviceNotifications, lib_exports };
43
+ export { DEFAULT_MARKUP_TIERS, DEFAULT_TAX_RATE, KFC_STATS_DEFAULT_WINDOW_DAYS, KFC_STATS_MAX_RANGE_DAYS, KFC_STATS_TIMEZONE, MAX_MESSAGE_ATTACHMENTS, SYSTEM_CATEGORIES, SYSTEM_CATEGORY_LABELS, billEffectiveDate, billGrandTotal, billTaxAmount, billTotal, billTotals, categoryForDevice, derivePrice, effectiveTaxRate, formatMoney, getAllowedDeviceTypes, getAllowedFeatures, getAllowedResourceIds, inferDeviceNotifications, isWellFormedRichText, keyedMarkupPercent, lib_exports, resolveBillTaxExempt, resolveKfcStatsWindowBounds, resolveMarkupTierTable, serializeRichTextBody, settledBillTotal, taxableSubtotal };
19
44
 
20
45
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../.generated/lib/index.ts"],"sourcesContent":["export {\n inferDeviceNotifications,\n type DeviceNotificationName,\n type InferDeviceNotificationsInput,\n} from \"./deviceNotifications.js\";\nexport {\n categoryForDevice,\n SYSTEM_CATEGORIES,\n SYSTEM_CATEGORY_LABELS,\n type SystemCategory,\n type CategoryForDeviceInput,\n} from \"./systemCategories.js\";\nexport {\n getAllowedResourceIds,\n getAllowedDeviceTypes,\n getAllowedFeatures,\n type PolicyLike,\n type PolicyPermissionLike,\n} from \"./policy-resolvers.js\";\nexport { MAX_MESSAGE_ATTACHMENTS } from \"./messageAttachments.js\";\n"],"mappings":""}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../.generated/lib/index.ts"],"sourcesContent":["export {\n inferDeviceNotifications,\n type DeviceNotificationName,\n type InferDeviceNotificationsInput,\n} from \"./deviceNotifications.js\";\nexport {\n categoryForDevice,\n SYSTEM_CATEGORIES,\n SYSTEM_CATEGORY_LABELS,\n type SystemCategory,\n type CategoryForDeviceInput,\n} from \"./systemCategories.js\";\nexport {\n getAllowedResourceIds,\n getAllowedDeviceTypes,\n getAllowedFeatures,\n type PolicyLike,\n type PolicyPermissionLike,\n} from \"./policy-resolvers.js\";\nexport { MAX_MESSAGE_ATTACHMENTS } from \"./messageAttachments.js\";\nexport {\n DEFAULT_MARKUP_TIERS,\n resolveMarkupTierTable,\n keyedMarkupPercent,\n DEFAULT_TAX_RATE,\n derivePrice,\n billTotal,\n billTaxAmount,\n billGrandTotal,\n formatMoney,\n} from \"./money.js\";\nexport {\n effectiveTaxRate,\n taxableSubtotal,\n billTotals,\n resolveBillTaxExempt,\n settledBillTotal,\n billEffectiveDate,\n type BillTaxFields,\n type TaxableLine,\n type SettledBillTotalSource,\n type BillEffectiveDateSource,\n} from \"./bills.js\";\nexport {\n KFC_STATS_TIMEZONE,\n KFC_STATS_DEFAULT_WINDOW_DAYS,\n KFC_STATS_MAX_RANGE_DAYS,\n resolveKfcStatsWindowBounds,\n type KfcStatsWindowBounds,\n} from \"./kfcStatsWindow.js\";\nexport {\n serializeRichTextBody,\n isWellFormedRichText,\n type RichTextContent,\n type RichTextBlockNode,\n type RichTextInlineNode,\n type RichTextListItemNode,\n type RichTextMentionNode,\n} from \"./ticketRichText.js\";\n"],"mappings":""}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The KFC stats date-window contract, shared verbatim by the API endpoint and
3
+ * the admin console so a `from`/`to` pair means the same window on both sides.
4
+ * The server owns the authoritative resolution — it layers range validation and
5
+ * instant conversion on top — while the client owns the preset and comparison
6
+ * math the server has no notion of. Both must agree on the *constants and the
7
+ * fallback*, though: the default view carries no dates in the URL, so the client
8
+ * has to reconstruct the exact window the server would have chosen. Publishing
9
+ * them once here is what keeps the two from silently disagreeing when the window
10
+ * default or the anchor zone changes.
11
+ */
12
+ /** Calendar-day windows are anchored to this fixed zone (#360). */
13
+ export declare const KFC_STATS_TIMEZONE = "America/Los_Angeles";
14
+ /** Inclusive span of the default window when no `from` is given. */
15
+ export declare const KFC_STATS_DEFAULT_WINDOW_DAYS = 30;
16
+ /**
17
+ * Widest inclusive span a caller may request — a full leap year. Cross-org
18
+ * federated aggregation cost scales with the window, so this caps the work an
19
+ * arbitrary caller can ask for.
20
+ */
21
+ export declare const KFC_STATS_MAX_RANGE_DAYS = 366;
22
+ /** A resolved inclusive day window as `YYYY-MM-DD` civil-date labels. */
23
+ export interface KfcStatsWindowBounds {
24
+ from: string;
25
+ to: string;
26
+ }
27
+ /**
28
+ * The fallback both sides apply to a partial window: a missing `to` is `today`
29
+ * (already resolved to the anchor zone by the caller), and a missing `from` is
30
+ * the day `KFC_STATS_DEFAULT_WINDOW_DAYS - 1` before `to`, so the default view
31
+ * spans that many days inclusive of today. Explicit bounds pass through
32
+ * untouched; parsing, ordering, and span limits are the caller's to enforce.
33
+ */
34
+ export declare function resolveKfcStatsWindowBounds(window: {
35
+ from?: string | null;
36
+ to?: string | null;
37
+ }, today: string): KfcStatsWindowBounds;
@@ -0,0 +1,46 @@
1
+ //#region .generated/lib/kfcStatsWindow.ts
2
+ /**
3
+ * The KFC stats date-window contract, shared verbatim by the API endpoint and
4
+ * the admin console so a `from`/`to` pair means the same window on both sides.
5
+ * The server owns the authoritative resolution — it layers range validation and
6
+ * instant conversion on top — while the client owns the preset and comparison
7
+ * math the server has no notion of. Both must agree on the *constants and the
8
+ * fallback*, though: the default view carries no dates in the URL, so the client
9
+ * has to reconstruct the exact window the server would have chosen. Publishing
10
+ * them once here is what keeps the two from silently disagreeing when the window
11
+ * default or the anchor zone changes.
12
+ */
13
+ /** Calendar-day windows are anchored to this fixed zone (#360). */
14
+ const KFC_STATS_TIMEZONE = "America/Los_Angeles";
15
+ /** Inclusive span of the default window when no `from` is given. */
16
+ const KFC_STATS_DEFAULT_WINDOW_DAYS = 30;
17
+ /**
18
+ * Widest inclusive span a caller may request — a full leap year. Cross-org
19
+ * federated aggregation cost scales with the window, so this caps the work an
20
+ * arbitrary caller can ask for.
21
+ */
22
+ const KFC_STATS_MAX_RANGE_DAYS = 366;
23
+ const DAY_MS = 864e5;
24
+ function shiftDateLabel(label, days) {
25
+ const [year, month, day] = label.split("-").map(Number);
26
+ const shifted = new Date(Date.UTC(year, month - 1, day) + days * DAY_MS);
27
+ return `${String(shifted.getUTCFullYear()).padStart(4, "0")}-${String(shifted.getUTCMonth() + 1).padStart(2, "0")}-${String(shifted.getUTCDate()).padStart(2, "0")}`;
28
+ }
29
+ /**
30
+ * The fallback both sides apply to a partial window: a missing `to` is `today`
31
+ * (already resolved to the anchor zone by the caller), and a missing `from` is
32
+ * the day `KFC_STATS_DEFAULT_WINDOW_DAYS - 1` before `to`, so the default view
33
+ * spans that many days inclusive of today. Explicit bounds pass through
34
+ * untouched; parsing, ordering, and span limits are the caller's to enforce.
35
+ */
36
+ function resolveKfcStatsWindowBounds(window, today) {
37
+ const to = window.to ?? today;
38
+ return {
39
+ from: window.from ?? shiftDateLabel(to, -29),
40
+ to
41
+ };
42
+ }
43
+ //#endregion
44
+ export { KFC_STATS_DEFAULT_WINDOW_DAYS, KFC_STATS_MAX_RANGE_DAYS, KFC_STATS_TIMEZONE, resolveKfcStatsWindowBounds };
45
+
46
+ //# sourceMappingURL=kfcStatsWindow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kfcStatsWindow.js","names":[],"sources":["../../.generated/lib/kfcStatsWindow.ts"],"sourcesContent":["/**\n * The KFC stats date-window contract, shared verbatim by the API endpoint and\n * the admin console so a `from`/`to` pair means the same window on both sides.\n * The server owns the authoritative resolution — it layers range validation and\n * instant conversion on top — while the client owns the preset and comparison\n * math the server has no notion of. Both must agree on the *constants and the\n * fallback*, though: the default view carries no dates in the URL, so the client\n * has to reconstruct the exact window the server would have chosen. Publishing\n * them once here is what keeps the two from silently disagreeing when the window\n * default or the anchor zone changes.\n */\n\n/** Calendar-day windows are anchored to this fixed zone (#360). */\nexport const KFC_STATS_TIMEZONE = \"America/Los_Angeles\";\n\n/** Inclusive span of the default window when no `from` is given. */\nexport const KFC_STATS_DEFAULT_WINDOW_DAYS = 30;\n\n/**\n * Widest inclusive span a caller may request — a full leap year. Cross-org\n * federated aggregation cost scales with the window, so this caps the work an\n * arbitrary caller can ask for.\n */\nexport const KFC_STATS_MAX_RANGE_DAYS = 366;\n\n/** A resolved inclusive day window as `YYYY-MM-DD` civil-date labels. */\nexport interface KfcStatsWindowBounds {\n from: string;\n to: string;\n}\n\nconst DAY_MS = 86_400_000;\n\n// Civil-date arithmetic on UTC-anchored instants so a day shift never drifts\n// across a DST boundary; only the calendar fields are ever read back out.\nfunction shiftDateLabel(label: string, days: number): string {\n const [year, month, day] = label.split(\"-\").map(Number);\n const shifted = new Date(Date.UTC(year, month - 1, day) + days * DAY_MS);\n const y = String(shifted.getUTCFullYear()).padStart(4, \"0\");\n const m = String(shifted.getUTCMonth() + 1).padStart(2, \"0\");\n const d = String(shifted.getUTCDate()).padStart(2, \"0\");\n return `${y}-${m}-${d}`;\n}\n\n/**\n * The fallback both sides apply to a partial window: a missing `to` is `today`\n * (already resolved to the anchor zone by the caller), and a missing `from` is\n * the day `KFC_STATS_DEFAULT_WINDOW_DAYS - 1` before `to`, so the default view\n * spans that many days inclusive of today. Explicit bounds pass through\n * untouched; parsing, ordering, and span limits are the caller's to enforce.\n */\nexport function resolveKfcStatsWindowBounds(\n window: { from?: string | null; to?: string | null },\n today: string,\n): KfcStatsWindowBounds {\n const to = window.to ?? today;\n const from =\n window.from ?? shiftDateLabel(to, -(KFC_STATS_DEFAULT_WINDOW_DAYS - 1));\n return { from, to };\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,qBAAqB;;AAGlC,MAAa,gCAAgC;;;;;;AAO7C,MAAa,2BAA2B;AAQxC,MAAM,SAAS;AAIf,SAAS,eAAe,OAAe,MAAsB;CAC3D,MAAM,CAAC,MAAM,OAAO,OAAO,MAAM,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM;CACtD,MAAM,UAAU,IAAI,KAAK,KAAK,IAAI,MAAM,QAAQ,GAAG,GAAG,IAAI,OAAO,MAAM;CAIvE,OAAO,GAHG,OAAO,QAAQ,eAAe,CAAC,CAAC,CAAC,SAAS,GAAG,GAG7C,EAAE,GAFF,OAAO,QAAQ,YAAY,IAAI,CAAC,CAAC,CAAC,SAAS,GAAG,GAEzC,EAAE,GADP,OAAO,QAAQ,WAAW,CAAC,CAAC,CAAC,SAAS,GAAG,GAC/B;AACtB;;;;;;;;AASA,SAAgB,4BACd,QACA,OACsB;CACtB,MAAM,KAAK,OAAO,MAAM;CAGxB,OAAO;EAAE,MADP,OAAO,QAAQ,eAAe,IAAI,GAAoC;EACzD;CAAG;AACpB"}
@@ -0,0 +1,11 @@
1
+ import type { Ajv } from "ajv";
2
+ export interface LowerBoundTableParams {
3
+ key: string;
4
+ }
5
+ /**
6
+ * Registers the `lowerBoundTable` keyword: on an array of objects,
7
+ * `lowerBoundTable: { key: "minAmount" }` enforces that the first row's key is
8
+ * 0 and keys strictly ascend, so a table of lower-bound rows can have no gaps
9
+ * or overlaps by construction. An empty array is valid (no table configured).
10
+ */
11
+ export declare function addLowerBoundTableKeyword(ajv: Ajv): Ajv;
@@ -0,0 +1,28 @@
1
+ //#region .generated/lib/lowerBoundTable.ts
2
+ /**
3
+ * Registers the `lowerBoundTable` keyword: on an array of objects,
4
+ * `lowerBoundTable: { key: "minAmount" }` enforces that the first row's key is
5
+ * 0 and keys strictly ascend, so a table of lower-bound rows can have no gaps
6
+ * or overlaps by construction. An empty array is valid (no table configured).
7
+ */
8
+ function addLowerBoundTableKeyword(ajv) {
9
+ return ajv.addKeyword({
10
+ keyword: "lowerBoundTable",
11
+ type: "array",
12
+ schemaType: "object",
13
+ validate(params, rows) {
14
+ let previous;
15
+ for (const row of rows) {
16
+ const value = typeof row === "object" && row !== null ? row[params.key] : void 0;
17
+ if (typeof value !== "number") return false;
18
+ if (previous === void 0 ? value !== 0 : value <= previous) return false;
19
+ previous = value;
20
+ }
21
+ return true;
22
+ }
23
+ });
24
+ }
25
+ //#endregion
26
+ export { addLowerBoundTableKeyword };
27
+
28
+ //# sourceMappingURL=lowerBoundTable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lowerBoundTable.js","names":[],"sources":["../../.generated/lib/lowerBoundTable.ts"],"sourcesContent":["import type { Ajv } from \"ajv\";\n\nexport interface LowerBoundTableParams {\n key: string;\n}\n\n/**\n * Registers the `lowerBoundTable` keyword: on an array of objects,\n * `lowerBoundTable: { key: \"minAmount\" }` enforces that the first row's key is\n * 0 and keys strictly ascend, so a table of lower-bound rows can have no gaps\n * or overlaps by construction. An empty array is valid (no table configured).\n */\nexport function addLowerBoundTableKeyword(ajv: Ajv): Ajv {\n return ajv.addKeyword({\n keyword: \"lowerBoundTable\",\n type: \"array\",\n schemaType: \"object\",\n validate(params: LowerBoundTableParams, rows: unknown[]) {\n let previous: number | undefined;\n for (const row of rows) {\n const value =\n typeof row === \"object\" && row !== null\n ? (row as Record<string, unknown>)[params.key]\n : undefined;\n if (typeof value !== \"number\") return false;\n if (previous === undefined ? value !== 0 : value <= previous) {\n return false;\n }\n previous = value;\n }\n return true;\n },\n });\n}\n"],"mappings":";;;;;;;AAYA,SAAgB,0BAA0B,KAAe;CACvD,OAAO,IAAI,WAAW;EACpB,SAAS;EACT,MAAM;EACN,YAAY;EACZ,SAAS,QAA+B,MAAiB;GACvD,IAAI;GACJ,KAAK,MAAM,OAAO,MAAM;IACtB,MAAM,QACJ,OAAO,QAAQ,YAAY,QAAQ,OAC9B,IAAgC,OAAO,OACxC,KAAA;IACN,IAAI,OAAO,UAAU,UAAU,OAAO;IACtC,IAAI,aAAa,KAAA,IAAY,UAAU,IAAI,SAAS,UAClD,OAAO;IAET,WAAW;GACb;GACA,OAAO;EACT;CACF,CAAC;AACH"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The money rules the API and the app must apply identically, so a price shown
3
+ * in the browser can never disagree with the price the server persists or the
4
+ * approval gate compares to the DNE. Before this module each rule was written
5
+ * once per side, and the duplication had already shipped a divergence between
6
+ * persisted copy and the live UI.
7
+ */
8
+ import type { MarkupTier } from "../schemas/organization.js";
9
+ /**
10
+ * The tier table an organization keys against before anyone configures one.
11
+ * KFC work is never billed at vendor cost, so an unconfigured organization
12
+ * keys to a single flat tier rather than to zero. A saved table replaces this
13
+ * outright — it is a fallback, not a floor, so an organization that should
14
+ * carry no markup writes an explicit `0` tier.
15
+ */
16
+ export declare const DEFAULT_MARKUP_TIERS: readonly MarkupTier[];
17
+ /** An organization's markup tiers, or the default when it has none of its own. */
18
+ export declare function resolveMarkupTierTable(tiers: readonly MarkupTier[] | undefined | null): MarkupTier[];
19
+ /**
20
+ * Keys a cost entry's markup percent from the org's lower-bound tier table by
21
+ * the entry's vendor-cost estimate (never the ticket total): the highest row
22
+ * whose `minAmount` the estimate meets. Null when no tiers are configured.
23
+ */
24
+ export declare function keyedMarkupPercent(tiers: readonly MarkupTier[], estimateAmount: number): number | null;
25
+ /**
26
+ * The marked-up price of a vendor cost, in integer cents. Price is never stored
27
+ * anywhere — it is always derived from vendor cost and markup on integer cents,
28
+ * so the two sides can never let the vendor cost, markup, and price disagree.
29
+ */
30
+ export declare function derivePrice(amount: number, percent: number): number;
31
+ /** The sum of already-derived line amounts, in integer cents. */
32
+ export declare function billTotal(lineAmounts: readonly number[]): number;
33
+ /**
34
+ * The tax rate (a plain float percent) a draft bill uses until a biller writes
35
+ * its own onto the bill.
36
+ */
37
+ export declare const DEFAULT_TAX_RATE = 8.375;
38
+ /**
39
+ * Tax in integer cents on a bill's taxable subtotal. Taxable credits reduce the
40
+ * base; when they push it negative the tax clamps at zero rather than going
41
+ * negative — a bill never emits a tax refund.
42
+ */
43
+ export declare function billTaxAmount(taxableSubtotalCents: number, ratePercent: number): number;
44
+ /** The bill's grand total in integer cents: line sum plus tax and shipping. */
45
+ export declare function billGrandTotal({ subtotal, tax, shipping, }: {
46
+ subtotal: number;
47
+ tax: number;
48
+ shipping: number;
49
+ }): number;
50
+ /**
51
+ * Render integer cents as a currency string, e.g.
52
+ * `formatMoney(123456, "USD")` → `"$1,234.56"`.
53
+ *
54
+ * Locale policy: `locale` defaults to `"en-US"` — the pin for any copy that is
55
+ * persisted or shipped to a fixed audience (bill PDFs, emails, and the stored
56
+ * approval/notification messages a customer reads back later). Live UI that
57
+ * should follow the viewer passes `locale = null`, which falls back to the
58
+ * runtime's default locale. Persisted-en-US / live-locale-aware is the
59
+ * deliberate split; a persisted caller must never inherit the browser locale.
60
+ */
61
+ export declare function formatMoney(cents: number, currency?: string, locale?: string | null): string;
@@ -0,0 +1,80 @@
1
+ //#region .generated/lib/money.ts
2
+ /**
3
+ * The tier table an organization keys against before anyone configures one.
4
+ * KFC work is never billed at vendor cost, so an unconfigured organization
5
+ * keys to a single flat tier rather than to zero. A saved table replaces this
6
+ * outright — it is a fallback, not a floor, so an organization that should
7
+ * carry no markup writes an explicit `0` tier.
8
+ */
9
+ const DEFAULT_MARKUP_TIERS = [{
10
+ minAmount: 0,
11
+ percent: 10
12
+ }];
13
+ /** An organization's markup tiers, or the default when it has none of its own. */
14
+ function resolveMarkupTierTable(tiers) {
15
+ if (tiers && tiers.length > 0) return [...tiers];
16
+ return DEFAULT_MARKUP_TIERS.map((tier) => ({ ...tier }));
17
+ }
18
+ /**
19
+ * Keys a cost entry's markup percent from the org's lower-bound tier table by
20
+ * the entry's vendor-cost estimate (never the ticket total): the highest row
21
+ * whose `minAmount` the estimate meets. Null when no tiers are configured.
22
+ */
23
+ function keyedMarkupPercent(tiers, estimateAmount) {
24
+ let match = null;
25
+ for (const tier of tiers) {
26
+ if (tier.minAmount > estimateAmount) continue;
27
+ if (match === null || tier.minAmount > match.minAmount) match = tier;
28
+ }
29
+ return match === null ? null : match.percent;
30
+ }
31
+ /**
32
+ * The marked-up price of a vendor cost, in integer cents. Price is never stored
33
+ * anywhere — it is always derived from vendor cost and markup on integer cents,
34
+ * so the two sides can never let the vendor cost, markup, and price disagree.
35
+ */
36
+ function derivePrice(amount, percent) {
37
+ return Math.round(amount * (1 + percent / 100));
38
+ }
39
+ /** The sum of already-derived line amounts, in integer cents. */
40
+ function billTotal(lineAmounts) {
41
+ return lineAmounts.reduce((sum, amount) => sum + amount, 0);
42
+ }
43
+ /**
44
+ * The tax rate (a plain float percent) a draft bill uses until a biller writes
45
+ * its own onto the bill.
46
+ */
47
+ const DEFAULT_TAX_RATE = 8.375;
48
+ /**
49
+ * Tax in integer cents on a bill's taxable subtotal. Taxable credits reduce the
50
+ * base; when they push it negative the tax clamps at zero rather than going
51
+ * negative — a bill never emits a tax refund.
52
+ */
53
+ function billTaxAmount(taxableSubtotalCents, ratePercent) {
54
+ return Math.max(0, Math.round(taxableSubtotalCents * ratePercent / 100));
55
+ }
56
+ /** The bill's grand total in integer cents: line sum plus tax and shipping. */
57
+ function billGrandTotal({ subtotal, tax, shipping }) {
58
+ return subtotal + tax + shipping;
59
+ }
60
+ /**
61
+ * Render integer cents as a currency string, e.g.
62
+ * `formatMoney(123456, "USD")` → `"$1,234.56"`.
63
+ *
64
+ * Locale policy: `locale` defaults to `"en-US"` — the pin for any copy that is
65
+ * persisted or shipped to a fixed audience (bill PDFs, emails, and the stored
66
+ * approval/notification messages a customer reads back later). Live UI that
67
+ * should follow the viewer passes `locale = null`, which falls back to the
68
+ * runtime's default locale. Persisted-en-US / live-locale-aware is the
69
+ * deliberate split; a persisted caller must never inherit the browser locale.
70
+ */
71
+ function formatMoney(cents, currency = "USD", locale = "en-US") {
72
+ return new Intl.NumberFormat(locale ?? void 0, {
73
+ style: "currency",
74
+ currency
75
+ }).format(cents / 100);
76
+ }
77
+ //#endregion
78
+ export { DEFAULT_MARKUP_TIERS, DEFAULT_TAX_RATE, billGrandTotal, billTaxAmount, billTotal, derivePrice, formatMoney, keyedMarkupPercent, resolveMarkupTierTable };
79
+
80
+ //# sourceMappingURL=money.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"money.js","names":[],"sources":["../../.generated/lib/money.ts"],"sourcesContent":["/**\n * The money rules the API and the app must apply identically, so a price shown\n * in the browser can never disagree with the price the server persists or the\n * approval gate compares to the DNE. Before this module each rule was written\n * once per side, and the duplication had already shipped a divergence between\n * persisted copy and the live UI.\n */\n\nimport type { MarkupTier } from \"../schemas/organization.js\";\n\n/**\n * The tier table an organization keys against before anyone configures one.\n * KFC work is never billed at vendor cost, so an unconfigured organization\n * keys to a single flat tier rather than to zero. A saved table replaces this\n * outright — it is a fallback, not a floor, so an organization that should\n * carry no markup writes an explicit `0` tier.\n */\nexport const DEFAULT_MARKUP_TIERS: readonly MarkupTier[] = [\n { minAmount: 0, percent: 10 },\n];\n\n/** An organization's markup tiers, or the default when it has none of its own. */\nexport function resolveMarkupTierTable(\n tiers: readonly MarkupTier[] | undefined | null,\n): MarkupTier[] {\n if (tiers && tiers.length > 0) return [...tiers];\n return DEFAULT_MARKUP_TIERS.map((tier) => ({ ...tier }));\n}\n\n/**\n * Keys a cost entry's markup percent from the org's lower-bound tier table by\n * the entry's vendor-cost estimate (never the ticket total): the highest row\n * whose `minAmount` the estimate meets. Null when no tiers are configured.\n */\nexport function keyedMarkupPercent(\n tiers: readonly MarkupTier[],\n estimateAmount: number,\n): number | null {\n let match: MarkupTier | null = null;\n for (const tier of tiers) {\n if (tier.minAmount > estimateAmount) continue;\n if (match === null || tier.minAmount > match.minAmount) match = tier;\n }\n return match === null ? null : match.percent;\n}\n\n/**\n * The marked-up price of a vendor cost, in integer cents. Price is never stored\n * anywhere — it is always derived from vendor cost and markup on integer cents,\n * so the two sides can never let the vendor cost, markup, and price disagree.\n */\nexport function derivePrice(amount: number, percent: number): number {\n return Math.round(amount * (1 + percent / 100));\n}\n\n/** The sum of already-derived line amounts, in integer cents. */\nexport function billTotal(lineAmounts: readonly number[]): number {\n return lineAmounts.reduce((sum, amount) => sum + amount, 0);\n}\n\n/**\n * The tax rate (a plain float percent) a draft bill uses until a biller writes\n * its own onto the bill.\n */\nexport const DEFAULT_TAX_RATE = 8.375;\n\n/**\n * Tax in integer cents on a bill's taxable subtotal. Taxable credits reduce the\n * base; when they push it negative the tax clamps at zero rather than going\n * negative — a bill never emits a tax refund.\n */\nexport function billTaxAmount(\n taxableSubtotalCents: number,\n ratePercent: number,\n): number {\n return Math.max(0, Math.round((taxableSubtotalCents * ratePercent) / 100));\n}\n\n/** The bill's grand total in integer cents: line sum plus tax and shipping. */\nexport function billGrandTotal({\n subtotal,\n tax,\n shipping,\n}: {\n subtotal: number;\n tax: number;\n shipping: number;\n}): number {\n return subtotal + tax + shipping;\n}\n\n/**\n * Render integer cents as a currency string, e.g.\n * `formatMoney(123456, \"USD\")` → `\"$1,234.56\"`.\n *\n * Locale policy: `locale` defaults to `\"en-US\"` — the pin for any copy that is\n * persisted or shipped to a fixed audience (bill PDFs, emails, and the stored\n * approval/notification messages a customer reads back later). Live UI that\n * should follow the viewer passes `locale = null`, which falls back to the\n * runtime's default locale. Persisted-en-US / live-locale-aware is the\n * deliberate split; a persisted caller must never inherit the browser locale.\n */\nexport function formatMoney(\n cents: number,\n currency = \"USD\",\n locale: string | null = \"en-US\",\n): string {\n return new Intl.NumberFormat(locale ?? undefined, {\n style: \"currency\",\n currency,\n }).format(cents / 100);\n}\n"],"mappings":";;;;;;;;AAiBA,MAAa,uBAA8C,CACzD;CAAE,WAAW;CAAG,SAAS;AAAG,CAC9B;;AAGA,SAAgB,uBACd,OACc;CACd,IAAI,SAAS,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,KAAK;CAC/C,OAAO,qBAAqB,KAAK,UAAU,EAAE,GAAG,KAAK,EAAE;AACzD;;;;;;AAOA,SAAgB,mBACd,OACA,gBACe;CACf,IAAI,QAA2B;CAC/B,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,YAAY,gBAAgB;EACrC,IAAI,UAAU,QAAQ,KAAK,YAAY,MAAM,WAAW,QAAQ;CAClE;CACA,OAAO,UAAU,OAAO,OAAO,MAAM;AACvC;;;;;;AAOA,SAAgB,YAAY,QAAgB,SAAyB;CACnE,OAAO,KAAK,MAAM,UAAU,IAAI,UAAU,IAAI;AAChD;;AAGA,SAAgB,UAAU,aAAwC;CAChE,OAAO,YAAY,QAAQ,KAAK,WAAW,MAAM,QAAQ,CAAC;AAC5D;;;;;AAMA,MAAa,mBAAmB;;;;;;AAOhC,SAAgB,cACd,sBACA,aACQ;CACR,OAAO,KAAK,IAAI,GAAG,KAAK,MAAO,uBAAuB,cAAe,GAAG,CAAC;AAC3E;;AAGA,SAAgB,eAAe,EAC7B,UACA,KACA,YAKS;CACT,OAAO,WAAW,MAAM;AAC1B;;;;;;;;;;;;AAaA,SAAgB,YACd,OACA,WAAW,OACX,SAAwB,SAChB;CACR,OAAO,IAAI,KAAK,aAAa,UAAU,KAAA,GAAW;EAChD,OAAO;EACP;CACF,CAAC,CAAC,CAAC,OAAO,QAAQ,GAAG;AACvB"}
@@ -21,10 +21,11 @@ function getAllowedDeviceTypes(policies, spaceId) {
21
21
  for (const policy of policies) for (const permission of policy.permissions) for (const entity of permission.entities) {
22
22
  if (!matchesSpace(entity)) continue;
23
23
  const [, , deviceType] = entity.split(":");
24
- if (permission.effect === "Allow") if (!deviceType) {
25
- if (!allowed.includes("*")) allowed.push("*");
26
- } else allowed.push(deviceType);
27
- else denied.push(deviceType);
24
+ if (permission.effect === "Allow") {
25
+ if (!deviceType) {
26
+ if (!allowed.includes("*")) allowed.push("*");
27
+ } else allowed.push(deviceType);
28
+ } else denied.push(deviceType);
28
29
  }
29
30
  return allowed.filter((id) => !denied.includes(id));
30
31
  }
@@ -1 +1 @@
1
- {"version":3,"file":"policy-resolvers.js","names":[],"sources":["../../.generated/lib/policy-resolvers.ts"],"sourcesContent":["export interface PolicyPermissionLike {\n entities: string[];\n effect: \"Allow\" | \"Deny\";\n}\n\nexport interface PolicyLike {\n permissions: PolicyPermissionLike[];\n}\n\nconst FEATURE_PREFIX = \"feature:\";\n\nexport function getAllowedResourceIds(\n policies: PolicyLike[],\n resource: string,\n): string[] {\n const allowed: string[] = [];\n const denied: string[] = [];\n\n for (const policy of policies) {\n for (const permission of policy.permissions) {\n for (const entity of permission.entities) {\n if (!entity.startsWith(resource)) continue;\n\n const [, id, subsystem, deviceId] = entity.split(\":\");\n const hasFinerSegment = Boolean(subsystem) || Boolean(deviceId);\n\n if (permission.effect === \"Allow\") {\n allowed.push(id);\n } else if (!hasFinerSegment) {\n denied.push(id);\n }\n }\n }\n }\n\n return denied.length > 0\n ? allowed.filter((id) => !denied.includes(id))\n : allowed;\n}\n\nexport function getAllowedDeviceTypes(\n policies: PolicyLike[],\n spaceId: string,\n): string[] {\n const prefix = `space:${spaceId}:`;\n const exactMatch = `space:${spaceId}`;\n const matchesSpace = (res: string) =>\n res === exactMatch || res.startsWith(prefix) || res.startsWith(\"space:*\");\n\n const allowed: string[] = [];\n const denied: string[] = [];\n\n for (const policy of policies) {\n for (const permission of policy.permissions) {\n for (const entity of permission.entities) {\n if (!matchesSpace(entity)) continue;\n\n const [, , deviceType] = entity.split(\":\");\n if (permission.effect === \"Allow\") {\n if (!deviceType) {\n if (!allowed.includes(\"*\")) allowed.push(\"*\");\n } else {\n allowed.push(deviceType);\n }\n } else {\n denied.push(deviceType);\n }\n }\n }\n }\n\n return allowed.filter((id) => !denied.includes(id));\n}\n\nexport function getAllowedFeatures(policies: PolicyLike[]): string[] {\n const allowed: string[] = [];\n const denied: string[] = [];\n\n for (const policy of policies) {\n for (const permission of policy.permissions) {\n for (const entity of permission.entities) {\n if (!entity.startsWith(FEATURE_PREFIX)) continue;\n\n const key = entity.slice(FEATURE_PREFIX.length);\n if (!key) continue;\n\n if (permission.effect === \"Allow\") {\n if (!allowed.includes(key)) allowed.push(key);\n } else if (!denied.includes(key)) {\n denied.push(key);\n }\n }\n }\n }\n\n return allowed.filter((key) => !denied.includes(key));\n}\n"],"mappings":";AASA,MAAM,iBAAiB;AAEvB,SAAgB,sBACd,UACA,UACU;CACV,MAAM,UAAoB,CAAC;CAC3B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,UAAU,UACnB,KAAK,MAAM,cAAc,OAAO,aAC9B,KAAK,MAAM,UAAU,WAAW,UAAU;EACxC,IAAI,CAAC,OAAO,WAAW,QAAQ,GAAG;EAElC,MAAM,GAAG,IAAI,WAAW,YAAY,OAAO,MAAM,GAAG;EACpD,MAAM,kBAAkB,QAAQ,SAAS,KAAK,QAAQ,QAAQ;EAE9D,IAAI,WAAW,WAAW,SACxB,QAAQ,KAAK,EAAE;OACV,IAAI,CAAC,iBACV,OAAO,KAAK,EAAE;CAElB;CAIJ,OAAO,OAAO,SAAS,IACnB,QAAQ,QAAQ,OAAO,CAAC,OAAO,SAAS,EAAE,CAAC,IAC3C;AACN;AAEA,SAAgB,sBACd,UACA,SACU;CACV,MAAM,SAAS,SAAS,QAAQ;CAChC,MAAM,aAAa,SAAS;CAC5B,MAAM,gBAAgB,QACpB,QAAQ,cAAc,IAAI,WAAW,MAAM,KAAK,IAAI,WAAW,SAAS;CAE1E,MAAM,UAAoB,CAAC;CAC3B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,UAAU,UACnB,KAAK,MAAM,cAAc,OAAO,aAC9B,KAAK,MAAM,UAAU,WAAW,UAAU;EACxC,IAAI,CAAC,aAAa,MAAM,GAAG;EAE3B,MAAM,KAAK,cAAc,OAAO,MAAM,GAAG;EACzC,IAAI,WAAW,WAAW,SACxB,IAAI,CAAC,YACC;OAAA,CAAC,QAAQ,SAAS,GAAG,GAAG,QAAQ,KAAK,GAAG;EAAA,OAE5C,QAAQ,KAAK,UAAU;OAGzB,OAAO,KAAK,UAAU;CAE1B;CAIJ,OAAO,QAAQ,QAAQ,OAAO,CAAC,OAAO,SAAS,EAAE,CAAC;AACpD;AAEA,SAAgB,mBAAmB,UAAkC;CACnE,MAAM,UAAoB,CAAC;CAC3B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,UAAU,UACnB,KAAK,MAAM,cAAc,OAAO,aAC9B,KAAK,MAAM,UAAU,WAAW,UAAU;EACxC,IAAI,CAAC,OAAO,WAAW,cAAc,GAAG;EAExC,MAAM,MAAM,OAAO,MAAM,CAAqB;EAC9C,IAAI,CAAC,KAAK;EAEV,IAAI,WAAW,WAAW,SACpB;OAAA,CAAC,QAAQ,SAAS,GAAG,GAAG,QAAQ,KAAK,GAAG;EAAA,OACvC,IAAI,CAAC,OAAO,SAAS,GAAG,GAC7B,OAAO,KAAK,GAAG;CAEnB;CAIJ,OAAO,QAAQ,QAAQ,QAAQ,CAAC,OAAO,SAAS,GAAG,CAAC;AACtD"}
1
+ {"version":3,"file":"policy-resolvers.js","names":[],"sources":["../../.generated/lib/policy-resolvers.ts"],"sourcesContent":["export interface PolicyPermissionLike {\n entities: string[];\n effect: \"Allow\" | \"Deny\";\n}\n\nexport interface PolicyLike {\n permissions: PolicyPermissionLike[];\n}\n\nconst FEATURE_PREFIX = \"feature:\";\n\nexport function getAllowedResourceIds(\n policies: PolicyLike[],\n resource: string,\n): string[] {\n const allowed: string[] = [];\n const denied: string[] = [];\n\n for (const policy of policies) {\n for (const permission of policy.permissions) {\n for (const entity of permission.entities) {\n if (!entity.startsWith(resource)) continue;\n\n const [, id, subsystem, deviceId] = entity.split(\":\");\n const hasFinerSegment = Boolean(subsystem) || Boolean(deviceId);\n\n if (permission.effect === \"Allow\") {\n allowed.push(id);\n } else if (!hasFinerSegment) {\n denied.push(id);\n }\n }\n }\n }\n\n return denied.length > 0\n ? allowed.filter((id) => !denied.includes(id))\n : allowed;\n}\n\nexport function getAllowedDeviceTypes(\n policies: PolicyLike[],\n spaceId: string,\n): string[] {\n const prefix = `space:${spaceId}:`;\n const exactMatch = `space:${spaceId}`;\n const matchesSpace = (res: string) =>\n res === exactMatch || res.startsWith(prefix) || res.startsWith(\"space:*\");\n\n const allowed: string[] = [];\n const denied: string[] = [];\n\n for (const policy of policies) {\n for (const permission of policy.permissions) {\n for (const entity of permission.entities) {\n if (!matchesSpace(entity)) continue;\n\n const [, , deviceType] = entity.split(\":\");\n if (permission.effect === \"Allow\") {\n if (!deviceType) {\n if (!allowed.includes(\"*\")) allowed.push(\"*\");\n } else {\n allowed.push(deviceType);\n }\n } else {\n denied.push(deviceType);\n }\n }\n }\n }\n\n return allowed.filter((id) => !denied.includes(id));\n}\n\nexport function getAllowedFeatures(policies: PolicyLike[]): string[] {\n const allowed: string[] = [];\n const denied: string[] = [];\n\n for (const policy of policies) {\n for (const permission of policy.permissions) {\n for (const entity of permission.entities) {\n if (!entity.startsWith(FEATURE_PREFIX)) continue;\n\n const key = entity.slice(FEATURE_PREFIX.length);\n if (!key) continue;\n\n if (permission.effect === \"Allow\") {\n if (!allowed.includes(key)) allowed.push(key);\n } else if (!denied.includes(key)) {\n denied.push(key);\n }\n }\n }\n }\n\n return allowed.filter((key) => !denied.includes(key));\n}\n"],"mappings":";AASA,MAAM,iBAAiB;AAEvB,SAAgB,sBACd,UACA,UACU;CACV,MAAM,UAAoB,CAAC;CAC3B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,UAAU,UACnB,KAAK,MAAM,cAAc,OAAO,aAC9B,KAAK,MAAM,UAAU,WAAW,UAAU;EACxC,IAAI,CAAC,OAAO,WAAW,QAAQ,GAAG;EAElC,MAAM,GAAG,IAAI,WAAW,YAAY,OAAO,MAAM,GAAG;EACpD,MAAM,kBAAkB,QAAQ,SAAS,KAAK,QAAQ,QAAQ;EAE9D,IAAI,WAAW,WAAW,SACxB,QAAQ,KAAK,EAAE;OACV,IAAI,CAAC,iBACV,OAAO,KAAK,EAAE;CAElB;CAIJ,OAAO,OAAO,SAAS,IACnB,QAAQ,QAAQ,OAAO,CAAC,OAAO,SAAS,EAAE,CAAC,IAC3C;AACN;AAEA,SAAgB,sBACd,UACA,SACU;CACV,MAAM,SAAS,SAAS,QAAQ;CAChC,MAAM,aAAa,SAAS;CAC5B,MAAM,gBAAgB,QACpB,QAAQ,cAAc,IAAI,WAAW,MAAM,KAAK,IAAI,WAAW,SAAS;CAE1E,MAAM,UAAoB,CAAC;CAC3B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,UAAU,UACnB,KAAK,MAAM,cAAc,OAAO,aAC9B,KAAK,MAAM,UAAU,WAAW,UAAU;EACxC,IAAI,CAAC,aAAa,MAAM,GAAG;EAE3B,MAAM,KAAK,cAAc,OAAO,MAAM,GAAG;EACzC,IAAI,WAAW,WAAW,SAAS;GACjC,IAAI,CAAC,YACC;QAAA,CAAC,QAAQ,SAAS,GAAG,GAAG,QAAQ,KAAK,GAAG;GAAA,OAE5C,QAAQ,KAAK,UAAU;EAE3B,OACE,OAAO,KAAK,UAAU;CAE1B;CAIJ,OAAO,QAAQ,QAAQ,OAAO,CAAC,OAAO,SAAS,EAAE,CAAC;AACpD;AAEA,SAAgB,mBAAmB,UAAkC;CACnE,MAAM,UAAoB,CAAC;CAC3B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,UAAU,UACnB,KAAK,MAAM,cAAc,OAAO,aAC9B,KAAK,MAAM,UAAU,WAAW,UAAU;EACxC,IAAI,CAAC,OAAO,WAAW,cAAc,GAAG;EAExC,MAAM,MAAM,OAAO,MAAM,CAAqB;EAC9C,IAAI,CAAC,KAAK;EAEV,IAAI,WAAW,WAAW,SACpB;OAAA,CAAC,QAAQ,SAAS,GAAG,GAAG,QAAQ,KAAK,GAAG;EAAA,OACvC,IAAI,CAAC,OAAO,SAAS,GAAG,GAC7B,OAAO,KAAK,GAAG;CAEnB;CAIJ,OAAO,QAAQ,QAAQ,QAAQ,CAAC,OAAO,SAAS,GAAG,CAAC;AACtD"}