@dainamite/billing 0.17.0 → 0.19.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 (206) hide show
  1. package/CHANGELOG.md +99 -19
  2. package/dist/modules/billing/acl.js +9 -1
  3. package/dist/modules/billing/acl.js.map +2 -2
  4. package/dist/modules/billing/api/accounts/[id]/balance/route.js +62 -0
  5. package/dist/modules/billing/api/accounts/[id]/balance/route.js.map +7 -0
  6. package/dist/modules/billing/api/accounts/[id]/credit-status/route.js +56 -0
  7. package/dist/modules/billing/api/accounts/[id]/credit-status/route.js.map +7 -0
  8. package/dist/modules/billing/api/accounts/[id]/topups/route.js +36 -0
  9. package/dist/modules/billing/api/accounts/[id]/topups/route.js.map +7 -0
  10. package/dist/modules/billing/api/accounts/route.js +7 -0
  11. package/dist/modules/billing/api/accounts/route.js.map +2 -2
  12. package/dist/modules/billing/api/export/account/[id]/route.js +48 -3
  13. package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
  14. package/dist/modules/billing/api/statements/generate/route.js +68 -0
  15. package/dist/modules/billing/api/statements/generate/route.js.map +7 -0
  16. package/dist/modules/billing/api/statements/route.js +110 -0
  17. package/dist/modules/billing/api/statements/route.js.map +7 -0
  18. package/dist/modules/billing/api/topups/route.js +139 -0
  19. package/dist/modules/billing/api/topups/route.js.map +7 -0
  20. package/dist/modules/billing/api/transactions/adjust/route.js +71 -0
  21. package/dist/modules/billing/api/transactions/adjust/route.js.map +7 -0
  22. package/dist/modules/billing/api/transactions/route.js +109 -0
  23. package/dist/modules/billing/api/transactions/route.js.map +7 -0
  24. package/dist/modules/billing/api/usage/route.js +30 -6
  25. package/dist/modules/billing/api/usage/route.js.map +2 -2
  26. package/dist/modules/billing/backend/billing/accounts/[id]/page.js +16 -1
  27. package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
  28. package/dist/modules/billing/backend/billing/accounts/create/page.js +8 -1
  29. package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
  30. package/dist/modules/billing/backend/billing/accounts/page.js +26 -1
  31. package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
  32. package/dist/modules/billing/backend/billing/statements/[id]/page.js +129 -0
  33. package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +7 -0
  34. package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js +21 -0
  35. package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js.map +7 -0
  36. package/dist/modules/billing/backend/billing/statements/page.js +152 -0
  37. package/dist/modules/billing/backend/billing/statements/page.js.map +7 -0
  38. package/dist/modules/billing/backend/billing/statements/page.meta.js +18 -0
  39. package/dist/modules/billing/backend/billing/statements/page.meta.js.map +7 -0
  40. package/dist/modules/billing/backend/billing/topups/[id]/page.js +90 -0
  41. package/dist/modules/billing/backend/billing/topups/[id]/page.js.map +7 -0
  42. package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js +21 -0
  43. package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js.map +7 -0
  44. package/dist/modules/billing/backend/billing/topups/page.js +147 -0
  45. package/dist/modules/billing/backend/billing/topups/page.js.map +7 -0
  46. package/dist/modules/billing/backend/billing/topups/page.meta.js +18 -0
  47. package/dist/modules/billing/backend/billing/topups/page.meta.js.map +7 -0
  48. package/dist/modules/billing/backend/billing/transactions/[id]/page.js +114 -0
  49. package/dist/modules/billing/backend/billing/transactions/[id]/page.js.map +7 -0
  50. package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js +21 -0
  51. package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js.map +7 -0
  52. package/dist/modules/billing/backend/billing/transactions/page.js +149 -0
  53. package/dist/modules/billing/backend/billing/transactions/page.js.map +7 -0
  54. package/dist/modules/billing/backend/billing/transactions/page.meta.js +18 -0
  55. package/dist/modules/billing/backend/billing/transactions/page.meta.js.map +7 -0
  56. package/dist/modules/billing/commands/accounts.js +26 -1
  57. package/dist/modules/billing/commands/accounts.js.map +2 -2
  58. package/dist/modules/billing/commands/index.js +3 -0
  59. package/dist/modules/billing/commands/index.js.map +2 -2
  60. package/dist/modules/billing/commands/invoices.js +21 -1
  61. package/dist/modules/billing/commands/invoices.js.map +2 -2
  62. package/dist/modules/billing/commands/statements.js +74 -0
  63. package/dist/modules/billing/commands/statements.js.map +7 -0
  64. package/dist/modules/billing/commands/topups.js +139 -0
  65. package/dist/modules/billing/commands/topups.js.map +7 -0
  66. package/dist/modules/billing/commands/transactions.js +119 -0
  67. package/dist/modules/billing/commands/transactions.js.map +7 -0
  68. package/dist/modules/billing/commands/usage.js +72 -0
  69. package/dist/modules/billing/commands/usage.js.map +2 -2
  70. package/dist/modules/billing/components/AccountForm.js +53 -1
  71. package/dist/modules/billing/components/AccountForm.js.map +2 -2
  72. package/dist/modules/billing/components/PrepaidPanel.js +314 -0
  73. package/dist/modules/billing/components/PrepaidPanel.js.map +7 -0
  74. package/dist/modules/billing/components/accountNames.js +20 -0
  75. package/dist/modules/billing/components/accountNames.js.map +7 -0
  76. package/dist/modules/billing/data/entities.js +290 -0
  77. package/dist/modules/billing/data/entities.js.map +2 -2
  78. package/dist/modules/billing/data/entityIds.js +6 -1
  79. package/dist/modules/billing/data/entityIds.js.map +2 -2
  80. package/dist/modules/billing/data/validators.js +107 -2
  81. package/dist/modules/billing/data/validators.js.map +2 -2
  82. package/dist/modules/billing/events.js +52 -0
  83. package/dist/modules/billing/events.js.map +2 -2
  84. package/dist/modules/billing/i18n/en.json +112 -4
  85. package/dist/modules/billing/i18n/pl.json +112 -4
  86. package/dist/modules/billing/index.js +1 -1
  87. package/dist/modules/billing/index.js.map +2 -2
  88. package/dist/modules/billing/lib/accountStatus.js +106 -0
  89. package/dist/modules/billing/lib/accountStatus.js.map +7 -0
  90. package/dist/modules/billing/lib/balanceEvents.js +48 -0
  91. package/dist/modules/billing/lib/balanceEvents.js.map +7 -0
  92. package/dist/modules/billing/lib/balanceLedger.js +136 -0
  93. package/dist/modules/billing/lib/balanceLedger.js.map +7 -0
  94. package/dist/modules/billing/lib/balanceStatus.js +29 -0
  95. package/dist/modules/billing/lib/balanceStatus.js.map +7 -0
  96. package/dist/modules/billing/lib/billRunEngine.js +40 -2
  97. package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
  98. package/dist/modules/billing/lib/creditStatus.js +65 -0
  99. package/dist/modules/billing/lib/creditStatus.js.map +7 -0
  100. package/dist/modules/billing/lib/money.js +47 -0
  101. package/dist/modules/billing/lib/money.js.map +7 -0
  102. package/dist/modules/billing/lib/prepaidConfig.js +45 -0
  103. package/dist/modules/billing/lib/prepaidConfig.js.map +7 -0
  104. package/dist/modules/billing/lib/prepaidConsume.js +214 -0
  105. package/dist/modules/billing/lib/prepaidConsume.js.map +7 -0
  106. package/dist/modules/billing/lib/prepaidPeriodClose.js +285 -0
  107. package/dist/modules/billing/lib/prepaidPeriodClose.js.map +7 -0
  108. package/dist/modules/billing/lib/topupCapture.js +62 -0
  109. package/dist/modules/billing/lib/topupCapture.js.map +7 -0
  110. package/dist/modules/billing/lib/topupReceipt.js +97 -0
  111. package/dist/modules/billing/lib/topupReceipt.js.map +7 -0
  112. package/dist/modules/billing/lib/topupTransition.js +19 -0
  113. package/dist/modules/billing/lib/topupTransition.js.map +7 -0
  114. package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js +110 -0
  115. package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js.map +7 -0
  116. package/dist/modules/billing/setup.js +32 -1
  117. package/dist/modules/billing/setup.js.map +2 -2
  118. package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js +26 -0
  119. package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js.map +7 -0
  120. package/dist/modules/billing/subscribers/prepaid-topup-captured.js +87 -0
  121. package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +7 -0
  122. package/dist/modules/billing/subscribers/prepaid-topup-expired.js +26 -0
  123. package/dist/modules/billing/subscribers/prepaid-topup-expired.js.map +7 -0
  124. package/dist/modules/billing/subscribers/prepaid-topup-failed.js +26 -0
  125. package/dist/modules/billing/subscribers/prepaid-topup-failed.js.map +7 -0
  126. package/dist/modules/billing/workers/reconcile-prepaid-balances.js +50 -0
  127. package/dist/modules/billing/workers/reconcile-prepaid-balances.js.map +7 -0
  128. package/package.json +9 -9
  129. package/src/modules/billing/__tests__/commands/invoices.test.ts +3 -3
  130. package/src/modules/billing/__tests__/commands/topups.test.ts +104 -0
  131. package/src/modules/billing/__tests__/commands/transactions.test.ts +107 -0
  132. package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +235 -0
  133. package/src/modules/billing/__tests__/lib/accountStatus.test.ts +93 -0
  134. package/src/modules/billing/__tests__/lib/balanceLedger.test.ts +125 -0
  135. package/src/modules/billing/__tests__/lib/balanceStatus.test.ts +65 -0
  136. package/src/modules/billing/__tests__/lib/creditStatus.test.ts +111 -0
  137. package/src/modules/billing/__tests__/lib/money.test.ts +56 -0
  138. package/src/modules/billing/__tests__/lib/prepaidConsume.test.ts +179 -0
  139. package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +232 -0
  140. package/src/modules/billing/__tests__/lib/topupCapture.test.ts +123 -0
  141. package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +84 -0
  142. package/src/modules/billing/__tests__/setup.test.ts +28 -2
  143. package/src/modules/billing/acl.ts +8 -0
  144. package/src/modules/billing/api/accounts/[id]/balance/route.ts +74 -0
  145. package/src/modules/billing/api/accounts/[id]/credit-status/route.ts +71 -0
  146. package/src/modules/billing/api/accounts/[id]/topups/route.ts +49 -0
  147. package/src/modules/billing/api/accounts/route.ts +7 -0
  148. package/src/modules/billing/api/export/account/[id]/route.ts +47 -2
  149. package/src/modules/billing/api/statements/generate/route.ts +78 -0
  150. package/src/modules/billing/api/statements/route.ts +117 -0
  151. package/src/modules/billing/api/topups/route.ts +154 -0
  152. package/src/modules/billing/api/transactions/adjust/route.ts +80 -0
  153. package/src/modules/billing/api/transactions/route.ts +116 -0
  154. package/src/modules/billing/api/usage/route.ts +36 -5
  155. package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +28 -0
  156. package/src/modules/billing/backend/billing/accounts/create/page.tsx +7 -0
  157. package/src/modules/billing/backend/billing/accounts/page.tsx +32 -1
  158. package/src/modules/billing/backend/billing/statements/[id]/page.meta.ts +19 -0
  159. package/src/modules/billing/backend/billing/statements/[id]/page.tsx +198 -0
  160. package/src/modules/billing/backend/billing/statements/page.meta.ts +16 -0
  161. package/src/modules/billing/backend/billing/statements/page.tsx +190 -0
  162. package/src/modules/billing/backend/billing/topups/[id]/page.meta.ts +19 -0
  163. package/src/modules/billing/backend/billing/topups/[id]/page.tsx +151 -0
  164. package/src/modules/billing/backend/billing/topups/page.meta.ts +16 -0
  165. package/src/modules/billing/backend/billing/topups/page.tsx +182 -0
  166. package/src/modules/billing/backend/billing/transactions/[id]/page.meta.ts +19 -0
  167. package/src/modules/billing/backend/billing/transactions/[id]/page.tsx +179 -0
  168. package/src/modules/billing/backend/billing/transactions/page.meta.ts +16 -0
  169. package/src/modules/billing/backend/billing/transactions/page.tsx +187 -0
  170. package/src/modules/billing/commands/accounts.ts +42 -1
  171. package/src/modules/billing/commands/index.ts +3 -0
  172. package/src/modules/billing/commands/invoices.ts +26 -2
  173. package/src/modules/billing/commands/statements.ts +101 -0
  174. package/src/modules/billing/commands/topups.ts +181 -0
  175. package/src/modules/billing/commands/transactions.ts +152 -0
  176. package/src/modules/billing/commands/usage.ts +114 -4
  177. package/src/modules/billing/components/AccountForm.tsx +66 -0
  178. package/src/modules/billing/components/PrepaidPanel.tsx +382 -0
  179. package/src/modules/billing/components/accountNames.ts +23 -0
  180. package/src/modules/billing/data/entities.ts +383 -0
  181. package/src/modules/billing/data/entityIds.ts +5 -0
  182. package/src/modules/billing/data/validators.ts +144 -0
  183. package/src/modules/billing/events.ts +52 -0
  184. package/src/modules/billing/i18n/en.json +112 -4
  185. package/src/modules/billing/i18n/pl.json +112 -4
  186. package/src/modules/billing/index.ts +1 -1
  187. package/src/modules/billing/lib/accountStatus.ts +155 -0
  188. package/src/modules/billing/lib/balanceEvents.ts +74 -0
  189. package/src/modules/billing/lib/balanceLedger.ts +258 -0
  190. package/src/modules/billing/lib/balanceStatus.ts +78 -0
  191. package/src/modules/billing/lib/billRunEngine.ts +66 -5
  192. package/src/modules/billing/lib/creditStatus.ts +153 -0
  193. package/src/modules/billing/lib/money.ts +73 -0
  194. package/src/modules/billing/lib/prepaidConfig.ts +58 -0
  195. package/src/modules/billing/lib/prepaidConsume.ts +299 -0
  196. package/src/modules/billing/lib/prepaidPeriodClose.ts +453 -0
  197. package/src/modules/billing/lib/topupCapture.ts +117 -0
  198. package/src/modules/billing/lib/topupReceipt.ts +153 -0
  199. package/src/modules/billing/lib/topupTransition.ts +31 -0
  200. package/src/modules/billing/migrations/Migration20260619000000_billing_prepaid.ts +131 -0
  201. package/src/modules/billing/setup.ts +45 -0
  202. package/src/modules/billing/subscribers/prepaid-topup-cancelled.ts +31 -0
  203. package/src/modules/billing/subscribers/prepaid-topup-captured.ts +120 -0
  204. package/src/modules/billing/subscribers/prepaid-topup-expired.ts +31 -0
  205. package/src/modules/billing/subscribers/prepaid-topup-failed.ts +32 -0
  206. package/src/modules/billing/workers/reconcile-prepaid-balances.ts +68 -0
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "billing.accounts.actions.open": "Otwórz",
3
+ "billing.accounts.columns.credit_limit": "Limit kredytowy",
3
4
  "billing.accounts.columns.currency": "Waluta",
4
5
  "billing.accounts.columns.customer": "Klient",
5
6
  "billing.accounts.columns.cycle": "Cykl",
6
7
  "billing.accounts.columns.last_bill": "Ostatnie rozliczenie",
8
+ "billing.accounts.columns.mode": "Tryb",
7
9
  "billing.accounts.columns.name": "Nazwa",
8
10
  "billing.accounts.columns.next_bill": "Następne rozliczenie",
9
11
  "billing.accounts.columns.status": "Status",
@@ -62,6 +64,8 @@
62
64
  "billing.accounts.form.error.next_bill_date_required": "Data następnego rozliczenia jest wymagana",
63
65
  "billing.accounts.form.invoice_email": "E-mail do faktur",
64
66
  "billing.accounts.form.invoice_language": "Język faktury (ISO 639-1)",
67
+ "billing.accounts.form.mode.locked": "Niezmienne — ustalane przy tworzeniu konta.",
68
+ "billing.accounts.form.mode.title": "Tryb rozliczeń i kredyt",
65
69
  "billing.accounts.form.name": "Nazwa",
66
70
  "billing.accounts.form.name.placeholder": "Uzupełnione z klienta — nadpisz np. „Acme — produkcja”",
67
71
  "billing.accounts.form.next_bill_date": "Data następnego rozliczenia",
@@ -69,6 +73,7 @@
69
73
  "billing.accounts.form.saving": "Zapisywanie…",
70
74
  "billing.accounts.form.submit_hint": "aby zapisać,",
71
75
  "billing.accounts.form.tax_id": "NIP / VATIN (opcjonalnie)",
76
+ "billing.accounts.form.threshold.hint": "Puste = domyślne dla najemcy",
72
77
  "billing.accounts.title": "Konta rozliczeniowe",
73
78
  "billing.common.active": "Aktywne",
74
79
  "billing.common.cancel": "Anuluj",
@@ -233,6 +238,37 @@
233
238
  "billing.items.type.one_time": "Jednorazowa",
234
239
  "billing.items.type.recurring": "Cykliczna",
235
240
  "billing.items.type.usage": "Wg zużycia",
241
+ "billing.mode.postpaid": "Postpłata",
242
+ "billing.mode.prepaid": "Przedpłata",
243
+ "billing.prepaid.credit.near_limit": "Blisko limitu",
244
+ "billing.prepaid.adjust.action": "Korekta",
245
+ "billing.prepaid.adjust.amount": "Kwota (+uznanie / −obciążenie)",
246
+ "billing.prepaid.adjust.confirm": "Zastosuj korektę",
247
+ "billing.prepaid.adjust.description": "Powód",
248
+ "billing.prepaid.adjust.error": "Korekta nie powiodła się",
249
+ "billing.prepaid.adjust.invalid_amount": "Podaj kwotę różną od zera",
250
+ "billing.prepaid.adjust.invalid_description": "Podaj powód",
251
+ "billing.prepaid.adjust.submitting": "Stosowanie…",
252
+ "billing.prepaid.adjust.success": "Saldo skorygowane",
253
+ "billing.prepaid.credit.over_limit": "Powyżej limitu",
254
+ "billing.prepaid.credit.within_limit": "W limicie",
255
+ "billing.prepaid.panel.balance": "Saldo",
256
+ "billing.prepaid.panel.credit": "Kredyt",
257
+ "billing.prepaid.panel.last_movement": "Ostatnia zmiana",
258
+ "billing.prepaid.panel.no_transactions": "Brak zmian salda.",
259
+ "billing.prepaid.panel.threshold": "Próg niskiego salda",
260
+ "billing.prepaid.panel.title": "Saldo przedpłacone",
261
+ "billing.prepaid.panel.transactions": "Transakcje salda",
262
+ "billing.prepaid.status.exhausted": "Wyczerpane",
263
+ "billing.prepaid.status.low": "Niskie",
264
+ "billing.prepaid.status.ok": "OK",
265
+ "billing.prepaid.topup.action": "Doładuj",
266
+ "billing.prepaid.topup.amount": "Kwota",
267
+ "billing.prepaid.topup.confirm": "Rozpocznij płatność",
268
+ "billing.prepaid.topup.created": "Utworzono sesję doładowania",
269
+ "billing.prepaid.topup.error": "Doładowanie nie powiodło się",
270
+ "billing.prepaid.topup.invalid": "Podaj dodatnią kwotę",
271
+ "billing.prepaid.topup.submitting": "Rozpoczynanie…",
236
272
  "billing.runs.actions.open": "Otwórz",
237
273
  "billing.runs.columns.accounts": "Konta",
238
274
  "billing.runs.columns.as_of_date": "Data rozliczenia",
@@ -256,6 +292,10 @@
256
292
  "billing.runs.detail.title": "Przebieg rozliczeniowy",
257
293
  "billing.runs.filters.status": "Status",
258
294
  "billing.runs.filters.triggered_by": "Wyzwalacz",
295
+ "billing.runs.mode.catch_up": "Nadrabianie",
296
+ "billing.runs.mode.dry_run": "Dry-run",
297
+ "billing.runs.mode.real": "Realny",
298
+ "billing.runs.mode.test": "Testowy",
259
299
  "billing.runs.outcomes.columns.account": "Konto",
260
300
  "billing.runs.outcomes.columns.error": "Błąd",
261
301
  "billing.runs.outcomes.columns.invoice": "Faktura",
@@ -277,10 +317,6 @@
277
317
  "billing.runs.summary.title": "Podsumowanie",
278
318
  "billing.runs.summary.usage": "Rozliczone zużycie",
279
319
  "billing.runs.summary.warnings": "Ostrzeżenia",
280
- "billing.runs.mode.catch_up": "Nadrabianie",
281
- "billing.runs.mode.dry_run": "Dry-run",
282
- "billing.runs.mode.real": "Realny",
283
- "billing.runs.mode.test": "Testowy",
284
320
  "billing.runs.title": "Przebiegi rozliczeniowe",
285
321
  "billing.runs.trigger.action": "Nowy przebieg",
286
322
  "billing.runs.trigger.dialog.as_of_date": "Data odniesienia",
@@ -299,6 +335,78 @@
299
335
  "billing.runs.trigger.manual": "Ręcznie",
300
336
  "billing.runs.trigger.schedule": "Harmonogram",
301
337
  "billing.runs.trigger.success": "Przebieg rozliczeniowy uruchomiony",
338
+ "billing.statements.breakdown.amount": "Kwota",
339
+ "billing.statements.breakdown.item": "Pozycja",
340
+ "billing.statements.breakdown.quantity": "Ilość",
341
+ "billing.statements.breakdown.type": "Typ",
342
+ "billing.statements.columns.adjustments": "Korekty",
343
+ "billing.statements.columns.closing": "Zamknięcie",
344
+ "billing.statements.columns.opening": "Otwarcie",
345
+ "billing.statements.columns.period": "Okres",
346
+ "billing.statements.columns.recurring": "Cykliczne",
347
+ "billing.statements.columns.status": "Status",
348
+ "billing.statements.columns.account": "Konto",
349
+ "billing.statements.columns.topups": "Doładowania",
350
+ "billing.statements.columns.usage": "Zużycie",
351
+ "billing.statements.detail.bill_run": "Przebieg rozliczeń",
352
+ "billing.statements.detail.breakdown": "Opłaty na koniec okresu",
353
+ "billing.statements.detail.load_error": "Nie udało się wczytać zestawienia",
354
+ "billing.statements.detail.loading": "Wczytywanie zestawienia…",
355
+ "billing.statements.detail.no_charges": "Brak opłat cyklicznych w tym okresie.",
356
+ "billing.statements.detail.not_found": "Nie znaleziono zestawienia",
357
+ "billing.statements.detail.title": "Zestawienie",
358
+ "billing.statements.detail.view_account": "Zobacz konto",
359
+ "billing.statements.filters.status": "Status",
360
+ "billing.statements.status.generated": "Wygenerowane",
361
+ "billing.statements.status.sent": "Wysłane",
362
+ "billing.statements.title": "Zestawienia",
363
+ "billing.topups.columns.account": "Konto",
364
+ "billing.topups.columns.amount": "Kwota",
365
+ "billing.topups.columns.created": "Utworzono",
366
+ "billing.topups.columns.provider": "Dostawca",
367
+ "billing.topups.columns.status": "Status",
368
+ "billing.topups.detail.balance_txn": "Transakcja salda",
369
+ "billing.topups.detail.gateway_txn": "Transakcja bramki",
370
+ "billing.topups.detail.load_error": "Nie udało się wczytać doładowania",
371
+ "billing.topups.detail.loading": "Wczytywanie doładowania…",
372
+ "billing.topups.detail.not_found": "Nie znaleziono doładowania",
373
+ "billing.topups.detail.payment_id": "Identyfikator płatności",
374
+ "billing.topups.detail.receipt": "Faktura VAT",
375
+ "billing.topups.detail.title": "Doładowanie",
376
+ "billing.topups.detail.updated": "Zaktualizowano",
377
+ "billing.topups.detail.view_account": "Zobacz konto",
378
+ "billing.topups.filters.status": "Status",
379
+ "billing.topups.status.cancelled": "Anulowane",
380
+ "billing.topups.status.captured": "Zaksięgowane",
381
+ "billing.topups.status.expired": "Wygasłe",
382
+ "billing.topups.status.failed": "Nieudane",
383
+ "billing.topups.status.pending": "Oczekujące",
384
+ "billing.topups.title": "Doładowania",
385
+ "billing.transactions.columns.amount": "Kwota",
386
+ "billing.transactions.columns.balance_after": "Saldo",
387
+ "billing.transactions.columns.description": "Opis",
388
+ "billing.transactions.columns.type": "Typ",
389
+ "billing.transactions.columns.when": "Kiedy",
390
+ "billing.transactions.type.adjustment": "Korekta",
391
+ "billing.transactions.type.one_time": "Jednorazowe",
392
+ "billing.transactions.type.recurring": "Cykliczne",
393
+ "billing.transactions.type.reversal": "Storno",
394
+ "billing.transactions.type.topup": "Doładowanie",
395
+ "billing.transactions.type.usage": "Zużycie",
396
+ "billing.transactions.columns.account": "Konto",
397
+ "billing.transactions.detail.item": "Pozycja rozliczeniowa",
398
+ "billing.transactions.detail.load_error": "Nie udało się wczytać transakcji",
399
+ "billing.transactions.detail.loading": "Wczytywanie transakcji…",
400
+ "billing.transactions.detail.not_found": "Nie znaleziono transakcji",
401
+ "billing.transactions.detail.source_ref": "Referencja źródłowa",
402
+ "billing.transactions.detail.statement": "Zestawienie",
403
+ "billing.transactions.detail.title": "Transakcja",
404
+ "billing.transactions.detail.topup": "Doładowanie",
405
+ "billing.transactions.detail.usage": "Rekord zużycia",
406
+ "billing.transactions.detail.user": "Użytkownik",
407
+ "billing.transactions.detail.view_account": "Zobacz konto",
408
+ "billing.transactions.filters.type": "Typ",
409
+ "billing.transactions.title": "Transakcje",
302
410
  "billing.usage.columns.description": "Opis",
303
411
  "billing.usage.columns.period": "Okres",
304
412
  "billing.usage.columns.quantity": "Ilość",
@@ -6,7 +6,7 @@ const metadata = {
6
6
  description: "Lightweight recurring-billing engine \u2014 collects one-time / recurring / usage items, runs Bill Runs, produces draft invoices in core/sales.",
7
7
  author: "Dainamite",
8
8
  license: "MIT",
9
- requires: ["auth", "directory", "dictionaries", "configs", "sales"],
9
+ requires: ["auth", "directory", "dictionaries", "configs", "sales", "payment_gateways"],
10
10
  ejectable: false
11
11
  };
12
12
  import { features } from "./acl.js";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/billing/index.ts"],
4
- "sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\n\n// Side-effect import \u2014 registers every CRUD command in the global\n// command bus at module load time. The makeCrudRoute factory looks up\n// `actions.<verb>.commandId` against this registry at request time, so\n// without this import every billing write returns \"command not found\".\n// Pattern mirrors `@open-mercato/core/src/modules/customers/index.ts`.\nimport './commands'\n\nexport const metadata: ModuleInfo = {\n name: 'billing',\n title: 'Billing',\n version: '0.1.0',\n description:\n 'Lightweight recurring-billing engine \u2014 collects one-time / recurring / usage items, runs Bill Runs, produces draft invoices in core/sales.',\n author: 'Dainamite',\n license: 'MIT',\n requires: ['auth', 'directory', 'dictionaries', 'configs', 'sales'],\n ejectable: false,\n}\n\nexport { features } from './acl'\n"],
5
- "mappings": "AAOA,OAAO;AAEA,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aACE;AAAA,EACF,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU,CAAC,QAAQ,aAAa,gBAAgB,WAAW,OAAO;AAAA,EAClE,WAAW;AACb;AAEA,SAAS,gBAAgB;",
4
+ "sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\n\n// Side-effect import \u2014 registers every CRUD command in the global\n// command bus at module load time. The makeCrudRoute factory looks up\n// `actions.<verb>.commandId` against this registry at request time, so\n// without this import every billing write returns \"command not found\".\n// Pattern mirrors `@open-mercato/core/src/modules/customers/index.ts`.\nimport './commands'\n\nexport const metadata: ModuleInfo = {\n name: 'billing',\n title: 'Billing',\n version: '0.1.0',\n description:\n 'Lightweight recurring-billing engine \u2014 collects one-time / recurring / usage items, runs Bill Runs, produces draft invoices in core/sales.',\n author: 'Dainamite',\n license: 'MIT',\n requires: ['auth', 'directory', 'dictionaries', 'configs', 'sales', 'payment_gateways'],\n ejectable: false,\n}\n\nexport { features } from './acl'\n"],
5
+ "mappings": "AAOA,OAAO;AAEA,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aACE;AAAA,EACF,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU,CAAC,QAAQ,aAAa,gBAAgB,WAAW,SAAS,kBAAkB;AAAA,EACtF,WAAW;AACb;AAEA,SAAS,gBAAgB;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,106 @@
1
+ import { BillingAccountBalance } from "../data/entities.js";
2
+ import {
3
+ computeBalanceStatus,
4
+ resolveLowBalanceThreshold
5
+ } from "./balanceStatus.js";
6
+ import {
7
+ computeCreditSnapshot,
8
+ detectCreditCrossing
9
+ } from "./creditStatus.js";
10
+ import { formatMoney, subMoney, toUnits } from "./money.js";
11
+ import {
12
+ getLowBalanceThresholdDefault,
13
+ getNearLimitBufferDefault
14
+ } from "./prepaidConfig.js";
15
+ async function getBalanceSnapshot(em, account) {
16
+ const row = await em.findOne(BillingAccountBalance, {
17
+ tenantId: account.tenantId,
18
+ billAccountId: account.id
19
+ });
20
+ const balance = row?.balance ?? "0.0000";
21
+ const threshold = resolveLowBalanceThreshold(
22
+ row?.lowBalanceThreshold ?? null,
23
+ await getLowBalanceThresholdDefault(em)
24
+ );
25
+ return {
26
+ billAccountId: account.id,
27
+ currencyCode: account.currencyCode,
28
+ balance: formatMoney(balance),
29
+ balanceStatus: computeBalanceStatus(balance, threshold),
30
+ lowBalanceThreshold: formatMoney(threshold),
31
+ lastMovementAt: row?.lastMovementAt ? row.lastMovementAt.toISOString() : null
32
+ };
33
+ }
34
+ async function getPostpaidOutstanding(em, account) {
35
+ const rows = await em.execute(
36
+ `SELECT COALESCE(SUM(outstanding_amount), 0) AS total
37
+ FROM sales_invoices
38
+ WHERE tenant_id = ?
39
+ AND organization_id = ?
40
+ AND deleted_at IS NULL
41
+ AND status = 'posted'
42
+ AND metadata->>'bill_account_id' = ?`,
43
+ [account.tenantId, account.organizationId, account.id]
44
+ );
45
+ return formatMoney(rows?.[0]?.total ?? 0);
46
+ }
47
+ async function getCreditSnapshot(em, account) {
48
+ const mode = account.billingMode ?? "postpaid";
49
+ const nearLimitBuffer = await getNearLimitBufferDefault(em);
50
+ let balance = null;
51
+ let outstanding = null;
52
+ if (mode === "prepaid") {
53
+ const row = await em.findOne(BillingAccountBalance, {
54
+ tenantId: account.tenantId,
55
+ billAccountId: account.id
56
+ });
57
+ balance = row?.balance ?? "0.0000";
58
+ } else {
59
+ outstanding = await getPostpaidOutstanding(em, account);
60
+ }
61
+ const snapshot = computeCreditSnapshot({
62
+ mode,
63
+ creditLimit: account.creditLimit ?? "0",
64
+ nearLimitBuffer,
65
+ balance,
66
+ outstanding
67
+ });
68
+ return { ...snapshot, currencyCode: account.currencyCode };
69
+ }
70
+ async function buildPostpaidCreditCrossingEvents(em, account, justPostedOutstanding) {
71
+ if (account.billingMode !== "postpaid") return [];
72
+ const creditLimit = account.creditLimit ?? "0";
73
+ if (toUnits(creditLimit) <= 0) return [];
74
+ const nearLimitBuffer = await getNearLimitBufferDefault(em);
75
+ const usedAfter = await getPostpaidOutstanding(em, account);
76
+ const usedBefore = subMoney(usedAfter, justPostedOutstanding);
77
+ const crossing = detectCreditCrossing({
78
+ creditLimit,
79
+ nearLimitBuffer,
80
+ usedBefore,
81
+ usedAfter
82
+ });
83
+ const events = [];
84
+ const base = {
85
+ billAccountId: account.id,
86
+ tenantId: account.tenantId,
87
+ organizationId: account.organizationId,
88
+ currencyCode: account.currencyCode,
89
+ creditLimit,
90
+ creditUsed: usedAfter
91
+ };
92
+ if (crossing.overLimitCrossedUp) {
93
+ events.push({ id: "billing.credit.over_limit", payload: { ...base } });
94
+ }
95
+ if (crossing.nearLimitCrossedUp) {
96
+ events.push({ id: "billing.credit.near_limit", payload: { ...base } });
97
+ }
98
+ return events;
99
+ }
100
+ export {
101
+ buildPostpaidCreditCrossingEvents,
102
+ getBalanceSnapshot,
103
+ getCreditSnapshot,
104
+ getPostpaidOutstanding
105
+ };
106
+ //# sourceMappingURL=accountStatus.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/billing/lib/accountStatus.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { BillingAccount, BillingAccountBalance } from '../data/entities'\nimport type { BalanceStatus, BillingMode } from '../data/validators'\nimport {\n computeBalanceStatus,\n resolveLowBalanceThreshold,\n} from './balanceStatus'\nimport {\n computeCreditSnapshot,\n detectCreditCrossing,\n type CreditSnapshot,\n} from './creditStatus'\nimport type { DomainEvent } from './balanceEvents'\nimport { formatMoney, subMoney, toUnits } from './money'\nimport {\n getLowBalanceThresholdDefault,\n getNearLimitBufferDefault,\n} from './prepaidConfig'\n\n/**\n * Read-model helpers for an account's prepaid balance + credit status\n * (SPEC-002). Shared by `GET \u2026/balance`, `GET \u2026/credit-status`, and the\n * admin Prepaid panel so the math lives in exactly one place.\n */\n\nexport type BalanceSnapshot = {\n billAccountId: string\n currencyCode: string\n balance: string\n balanceStatus: BalanceStatus\n /** The resolved threshold (account row \u2192 tenant config default). */\n lowBalanceThreshold: string\n lastMovementAt: string | null\n}\n\nexport async function getBalanceSnapshot(\n em: EntityManager,\n account: BillingAccount,\n): Promise<BalanceSnapshot> {\n const row = await em.findOne(BillingAccountBalance, {\n tenantId: account.tenantId,\n billAccountId: account.id,\n })\n const balance = row?.balance ?? '0.0000'\n const threshold = resolveLowBalanceThreshold(\n row?.lowBalanceThreshold ?? null,\n await getLowBalanceThresholdDefault(em),\n )\n return {\n billAccountId: account.id,\n currencyCode: account.currencyCode,\n balance: formatMoney(balance),\n balanceStatus: computeBalanceStatus(balance, threshold),\n lowBalanceThreshold: formatMoney(threshold),\n lastMovementAt: row?.lastMovementAt ? row.lastMovementAt.toISOString() : null,\n }\n}\n\n/**\n * \u03A3 outstanding of POSTED (not-yet-paid) core/sales invoices the engine\n * emitted for this account \u2014 the postpaid `credit_used` (v1; mid-period\n * accrued usage is not counted, see SPEC-002 Open Question #5). Top-up\n * receipts are `paid`, so they never count here.\n */\nexport async function getPostpaidOutstanding(\n em: EntityManager,\n account: BillingAccount,\n): Promise<string> {\n const rows = (await em.execute(\n `SELECT COALESCE(SUM(outstanding_amount), 0) AS total\n FROM sales_invoices\n WHERE tenant_id = ?\n AND organization_id = ?\n AND deleted_at IS NULL\n AND status = 'posted'\n AND metadata->>'bill_account_id' = ?`,\n [account.tenantId, account.organizationId, account.id],\n )) as unknown as Array<{ total: string }>\n return formatMoney(rows?.[0]?.total ?? 0)\n}\n\nexport type CreditSnapshotResult = CreditSnapshot & { currencyCode: string }\n\nexport async function getCreditSnapshot(\n em: EntityManager,\n account: BillingAccount,\n): Promise<CreditSnapshotResult> {\n const mode = (account.billingMode as BillingMode) ?? 'postpaid'\n const nearLimitBuffer = await getNearLimitBufferDefault(em)\n\n let balance: string | null = null\n let outstanding: string | null = null\n if (mode === 'prepaid') {\n const row = await em.findOne(BillingAccountBalance, {\n tenantId: account.tenantId,\n billAccountId: account.id,\n })\n balance = row?.balance ?? '0.0000'\n } else {\n outstanding = await getPostpaidOutstanding(em, account)\n }\n\n const snapshot = computeCreditSnapshot({\n mode,\n creditLimit: account.creditLimit ?? '0',\n nearLimitBuffer,\n balance,\n outstanding,\n })\n return { ...snapshot, currencyCode: account.currencyCode }\n}\n\n/**\n * Credit-crossing events for a postpaid account when a draft invoice is posted\n * (SPEC-002 P6). `billing.credit.over_limit` fires once on the upward crossing\n * of `credit_used` (\u03A3 posted-unpaid outstanding) past `credit_limit`. The\n * just-posted invoice's `outstanding` is already included in the current total,\n * so the prior total is `current \u2212 justPostedOutstanding`.\n */\nexport async function buildPostpaidCreditCrossingEvents(\n em: EntityManager,\n account: BillingAccount,\n justPostedOutstanding: string,\n): Promise<DomainEvent[]> {\n if ((account.billingMode as BillingMode) !== 'postpaid') return []\n const creditLimit = account.creditLimit ?? '0'\n if (toUnits(creditLimit) <= 0) return []\n\n const nearLimitBuffer = await getNearLimitBufferDefault(em)\n const usedAfter = await getPostpaidOutstanding(em, account)\n const usedBefore = subMoney(usedAfter, justPostedOutstanding)\n const crossing = detectCreditCrossing({\n creditLimit,\n nearLimitBuffer,\n usedBefore,\n usedAfter,\n })\n\n const events: DomainEvent[] = []\n const base = {\n billAccountId: account.id,\n tenantId: account.tenantId,\n organizationId: account.organizationId,\n currencyCode: account.currencyCode,\n creditLimit,\n creditUsed: usedAfter,\n }\n if (crossing.overLimitCrossedUp) {\n events.push({ id: 'billing.credit.over_limit', payload: { ...base } })\n }\n if (crossing.nearLimitCrossedUp) {\n events.push({ id: 'billing.credit.near_limit', payload: { ...base } })\n }\n return events\n}\n"],
5
+ "mappings": "AACA,SAAyB,6BAA6B;AAEtD;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AAEP,SAAS,aAAa,UAAU,eAAe;AAC/C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAkBP,eAAsB,mBACpB,IACA,SAC0B;AAC1B,QAAM,MAAM,MAAM,GAAG,QAAQ,uBAAuB;AAAA,IAClD,UAAU,QAAQ;AAAA,IAClB,eAAe,QAAQ;AAAA,EACzB,CAAC;AACD,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,YAAY;AAAA,IAChB,KAAK,uBAAuB;AAAA,IAC5B,MAAM,8BAA8B,EAAE;AAAA,EACxC;AACA,SAAO;AAAA,IACL,eAAe,QAAQ;AAAA,IACvB,cAAc,QAAQ;AAAA,IACtB,SAAS,YAAY,OAAO;AAAA,IAC5B,eAAe,qBAAqB,SAAS,SAAS;AAAA,IACtD,qBAAqB,YAAY,SAAS;AAAA,IAC1C,gBAAgB,KAAK,iBAAiB,IAAI,eAAe,YAAY,IAAI;AAAA,EAC3E;AACF;AAQA,eAAsB,uBACpB,IACA,SACiB;AACjB,QAAM,OAAQ,MAAM,GAAG;AAAA,IACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,CAAC,QAAQ,UAAU,QAAQ,gBAAgB,QAAQ,EAAE;AAAA,EACvD;AACA,SAAO,YAAY,OAAO,CAAC,GAAG,SAAS,CAAC;AAC1C;AAIA,eAAsB,kBACpB,IACA,SAC+B;AAC/B,QAAM,OAAQ,QAAQ,eAA+B;AACrD,QAAM,kBAAkB,MAAM,0BAA0B,EAAE;AAE1D,MAAI,UAAyB;AAC7B,MAAI,cAA6B;AACjC,MAAI,SAAS,WAAW;AACtB,UAAM,MAAM,MAAM,GAAG,QAAQ,uBAAuB;AAAA,MAClD,UAAU,QAAQ;AAAA,MAClB,eAAe,QAAQ;AAAA,IACzB,CAAC;AACD,cAAU,KAAK,WAAW;AAAA,EAC5B,OAAO;AACL,kBAAc,MAAM,uBAAuB,IAAI,OAAO;AAAA,EACxD;AAEA,QAAM,WAAW,sBAAsB;AAAA,IACrC;AAAA,IACA,aAAa,QAAQ,eAAe;AAAA,IACpC;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,SAAO,EAAE,GAAG,UAAU,cAAc,QAAQ,aAAa;AAC3D;AASA,eAAsB,kCACpB,IACA,SACA,uBACwB;AACxB,MAAK,QAAQ,gBAAgC,WAAY,QAAO,CAAC;AACjE,QAAM,cAAc,QAAQ,eAAe;AAC3C,MAAI,QAAQ,WAAW,KAAK,EAAG,QAAO,CAAC;AAEvC,QAAM,kBAAkB,MAAM,0BAA0B,EAAE;AAC1D,QAAM,YAAY,MAAM,uBAAuB,IAAI,OAAO;AAC1D,QAAM,aAAa,SAAS,WAAW,qBAAqB;AAC5D,QAAM,WAAW,qBAAqB;AAAA,IACpC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,QAAM,SAAwB,CAAC;AAC/B,QAAM,OAAO;AAAA,IACX,eAAe,QAAQ;AAAA,IACvB,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ;AAAA,IACxB,cAAc,QAAQ;AAAA,IACtB;AAAA,IACA,YAAY;AAAA,EACd;AACA,MAAI,SAAS,oBAAoB;AAC/B,WAAO,KAAK,EAAE,IAAI,6BAA6B,SAAS,EAAE,GAAG,KAAK,EAAE,CAAC;AAAA,EACvE;AACA,MAAI,SAAS,oBAAoB;AAC/B,WAAO,KAAK,EAAE,IAAI,6BAA6B,SAAS,EAAE,GAAG,KAAK,EAAE,CAAC;AAAA,EACvE;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,48 @@
1
+ import { detectBalanceCrossing } from "./balanceStatus.js";
2
+ import { detectCreditCrossing, prepaidCreditUsed } from "./creditStatus.js";
3
+ function buildBalanceCrossingEvents(params) {
4
+ const { scope, balanceBefore, balanceAfter, threshold, creditLimit, nearLimitBuffer } = params;
5
+ const events = [];
6
+ const base = {
7
+ billAccountId: scope.billAccountId,
8
+ tenantId: scope.tenantId,
9
+ organizationId: scope.organizationId,
10
+ currencyCode: scope.currencyCode,
11
+ balance: balanceAfter
12
+ };
13
+ const balanceCrossing = detectBalanceCrossing(balanceBefore, balanceAfter, threshold);
14
+ if (balanceCrossing.exhaustedCrossedDown) {
15
+ events.push({ id: "billing.balance.exhausted", payload: { ...base } });
16
+ }
17
+ if (balanceCrossing.lowCrossedDown) {
18
+ events.push({
19
+ id: "billing.balance.low",
20
+ payload: { ...base, lowBalanceThreshold: threshold }
21
+ });
22
+ }
23
+ const usedBefore = prepaidCreditUsed(balanceBefore);
24
+ const usedAfter = prepaidCreditUsed(balanceAfter);
25
+ const creditCrossing = detectCreditCrossing({
26
+ creditLimit,
27
+ nearLimitBuffer,
28
+ usedBefore,
29
+ usedAfter
30
+ });
31
+ if (creditCrossing.overLimitCrossedUp) {
32
+ events.push({
33
+ id: "billing.credit.over_limit",
34
+ payload: { ...base, creditLimit, creditUsed: usedAfter }
35
+ });
36
+ }
37
+ if (creditCrossing.nearLimitCrossedUp) {
38
+ events.push({
39
+ id: "billing.credit.near_limit",
40
+ payload: { ...base, creditLimit, creditUsed: usedAfter }
41
+ });
42
+ }
43
+ return events;
44
+ }
45
+ export {
46
+ buildBalanceCrossingEvents
47
+ };
48
+ //# sourceMappingURL=balanceEvents.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/billing/lib/balanceEvents.ts"],
4
+ "sourcesContent": ["import { detectBalanceCrossing } from './balanceStatus'\nimport { detectCreditCrossing, prepaidCreditUsed } from './creditStatus'\n\n/**\n * Shared balance/credit threshold-crossing \u2192 domain-event builder (SPEC-002).\n *\n * Every balance movement that can change status \u2014 real-time usage debit, a\n * manual adjustment, a period-close recurring charge \u2014 runs through this so\n * the \"fire only on the relevant downward/upward crossing\" rule lives in\n * exactly one place. The events are returned (not emitted) so callers fire\n * them AFTER the DB commit.\n */\n\nexport type DomainEvent = { id: string; payload: Record<string, unknown> }\n\nexport type BalanceEventScope = {\n billAccountId: string\n tenantId: string\n organizationId: string\n currencyCode: string\n}\n\nexport function buildBalanceCrossingEvents(params: {\n scope: BalanceEventScope\n balanceBefore: string\n balanceAfter: string\n threshold: string\n creditLimit: string\n nearLimitBuffer: string\n}): DomainEvent[] {\n const { scope, balanceBefore, balanceAfter, threshold, creditLimit, nearLimitBuffer } = params\n const events: DomainEvent[] = []\n const base = {\n billAccountId: scope.billAccountId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n currencyCode: scope.currencyCode,\n balance: balanceAfter,\n }\n\n const balanceCrossing = detectBalanceCrossing(balanceBefore, balanceAfter, threshold)\n if (balanceCrossing.exhaustedCrossedDown) {\n events.push({ id: 'billing.balance.exhausted', payload: { ...base } })\n }\n if (balanceCrossing.lowCrossedDown) {\n events.push({\n id: 'billing.balance.low',\n payload: { ...base, lowBalanceThreshold: threshold },\n })\n }\n\n const usedBefore = prepaidCreditUsed(balanceBefore)\n const usedAfter = prepaidCreditUsed(balanceAfter)\n const creditCrossing = detectCreditCrossing({\n creditLimit,\n nearLimitBuffer,\n usedBefore,\n usedAfter,\n })\n if (creditCrossing.overLimitCrossedUp) {\n events.push({\n id: 'billing.credit.over_limit',\n payload: { ...base, creditLimit, creditUsed: usedAfter },\n })\n }\n if (creditCrossing.nearLimitCrossedUp) {\n events.push({\n id: 'billing.credit.near_limit',\n payload: { ...base, creditLimit, creditUsed: usedAfter },\n })\n }\n\n return events\n}\n"],
5
+ "mappings": "AAAA,SAAS,6BAA6B;AACtC,SAAS,sBAAsB,yBAAyB;AAqBjD,SAAS,2BAA2B,QAOzB;AAChB,QAAM,EAAE,OAAO,eAAe,cAAc,WAAW,aAAa,gBAAgB,IAAI;AACxF,QAAM,SAAwB,CAAC;AAC/B,QAAM,OAAO;AAAA,IACX,eAAe,MAAM;AAAA,IACrB,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,cAAc,MAAM;AAAA,IACpB,SAAS;AAAA,EACX;AAEA,QAAM,kBAAkB,sBAAsB,eAAe,cAAc,SAAS;AACpF,MAAI,gBAAgB,sBAAsB;AACxC,WAAO,KAAK,EAAE,IAAI,6BAA6B,SAAS,EAAE,GAAG,KAAK,EAAE,CAAC;AAAA,EACvE;AACA,MAAI,gBAAgB,gBAAgB;AAClC,WAAO,KAAK;AAAA,MACV,IAAI;AAAA,MACJ,SAAS,EAAE,GAAG,MAAM,qBAAqB,UAAU;AAAA,IACrD,CAAC;AAAA,EACH;AAEA,QAAM,aAAa,kBAAkB,aAAa;AAClD,QAAM,YAAY,kBAAkB,YAAY;AAChD,QAAM,iBAAiB,qBAAqB;AAAA,IAC1C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,MAAI,eAAe,oBAAoB;AACrC,WAAO,KAAK;AAAA,MACV,IAAI;AAAA,MACJ,SAAS,EAAE,GAAG,MAAM,aAAa,YAAY,UAAU;AAAA,IACzD,CAAC;AAAA,EACH;AACA,MAAI,eAAe,oBAAoB;AACrC,WAAO,KAAK;AAAA,MACV,IAAI;AAAA,MACJ,SAAS,EAAE,GAAG,MAAM,aAAa,YAAY,UAAU;AAAA,IACzD,CAAC;AAAA,EACH;AAEA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,136 @@
1
+ import {
2
+ BillingAccountBalance,
3
+ BillingAccountTransaction
4
+ } from "../data/entities.js";
5
+ import { fromUnits, subMoney, toUnits } from "./money.js";
6
+ const PREPAID_USAGE_SENTINEL_RUN_ID = "dddddddd-dddd-4ddd-8ddd-dddddddddddd";
7
+ async function ensureBalanceRow(em, init) {
8
+ const existing = await em.findOne(BillingAccountBalance, {
9
+ tenantId: init.tenantId,
10
+ billAccountId: init.billAccountId
11
+ });
12
+ if (existing) {
13
+ if (init.lowBalanceThreshold !== void 0 && init.lowBalanceThreshold !== null && (existing.lowBalanceThreshold === null || existing.lowBalanceThreshold === void 0)) {
14
+ existing.lowBalanceThreshold = init.lowBalanceThreshold;
15
+ existing.updatedAt = /* @__PURE__ */ new Date();
16
+ }
17
+ return existing;
18
+ }
19
+ const now = /* @__PURE__ */ new Date();
20
+ const row = em.create(BillingAccountBalance, {
21
+ organizationId: init.organizationId,
22
+ tenantId: init.tenantId,
23
+ billAccountId: init.billAccountId,
24
+ currencyCode: init.currencyCode,
25
+ balance: "0",
26
+ lowBalanceThreshold: init.lowBalanceThreshold ?? null,
27
+ lastMovementAt: null,
28
+ createdAt: now,
29
+ updatedAt: now
30
+ });
31
+ em.persist(row);
32
+ await em.flush();
33
+ return row;
34
+ }
35
+ async function applyBalanceMovement(em, params) {
36
+ if (params.sourceRef) {
37
+ const existing = await em.findOne(BillingAccountTransaction, {
38
+ tenantId: params.tenantId,
39
+ billAccountId: params.billAccountId,
40
+ sourceRef: params.sourceRef,
41
+ deletedAt: null
42
+ });
43
+ if (existing) {
44
+ return {
45
+ balanceBefore: subMoney(existing.balanceAfter, existing.amount),
46
+ balanceAfter: existing.balanceAfter,
47
+ transaction: existing,
48
+ deduplicated: true
49
+ };
50
+ }
51
+ }
52
+ const now = /* @__PURE__ */ new Date();
53
+ await em.execute(
54
+ `INSERT INTO billing_account_balances
55
+ (id, organization_id, tenant_id, bill_account_id, currency_code, balance, created_at, updated_at)
56
+ VALUES (gen_random_uuid(), ?, ?, ?, ?, 0, ?, ?)
57
+ ON CONFLICT (tenant_id, bill_account_id) DO NOTHING`,
58
+ [
59
+ params.organizationId,
60
+ params.tenantId,
61
+ params.billAccountId,
62
+ params.currencyCode,
63
+ now,
64
+ now
65
+ ]
66
+ );
67
+ const rows = await em.execute(
68
+ `UPDATE billing_account_balances
69
+ SET balance = balance + ?, last_movement_at = ?, updated_at = ?
70
+ WHERE tenant_id = ? AND bill_account_id = ?
71
+ RETURNING balance`,
72
+ [params.amount, now, now, params.tenantId, params.billAccountId]
73
+ );
74
+ if (!rows || rows.length === 0) {
75
+ throw new Error(
76
+ `balanceLedger: balance row missing for account ${params.billAccountId} after upsert`
77
+ );
78
+ }
79
+ const balanceAfter = fromUnits(toUnits(rows[0].balance));
80
+ const balanceBefore = subMoney(balanceAfter, params.amount);
81
+ const transaction = em.create(BillingAccountTransaction, {
82
+ organizationId: params.organizationId,
83
+ tenantId: params.tenantId,
84
+ billAccountId: params.billAccountId,
85
+ type: params.type,
86
+ amount: params.amount,
87
+ currencyCode: params.currencyCode,
88
+ balanceAfter,
89
+ usageId: params.usageId ?? null,
90
+ billingItemId: params.billingItemId ?? null,
91
+ topupId: params.topupId ?? null,
92
+ statementId: params.statementId ?? null,
93
+ description: params.description,
94
+ sourceRef: params.sourceRef ?? null,
95
+ metadata: params.metadata ?? null,
96
+ userId: params.userId ?? null,
97
+ createdAt: now
98
+ });
99
+ em.persist(transaction);
100
+ await em.flush();
101
+ return { balanceBefore, balanceAfter, transaction, deduplicated: false };
102
+ }
103
+ async function reconcileAccountBalance(em, scope) {
104
+ const balanceRow = await em.findOne(BillingAccountBalance, {
105
+ tenantId: scope.tenantId,
106
+ billAccountId: scope.billAccountId
107
+ });
108
+ const cachedBalance = balanceRow ? fromUnits(toUnits(balanceRow.balance)) : "0.0000";
109
+ const sumRows = await em.execute(
110
+ `SELECT COALESCE(SUM(amount), 0) AS sum_amount
111
+ FROM billing_account_transactions
112
+ WHERE tenant_id = ? AND organization_id = ? AND bill_account_id = ? AND deleted_at IS NULL`,
113
+ [scope.tenantId, scope.organizationId, scope.billAccountId]
114
+ );
115
+ const sumOfAmounts = fromUnits(toUnits(sumRows?.[0]?.sum_amount ?? 0));
116
+ const latest = await em.findOne(
117
+ BillingAccountTransaction,
118
+ {
119
+ tenantId: scope.tenantId,
120
+ organizationId: scope.organizationId,
121
+ billAccountId: scope.billAccountId,
122
+ deletedAt: null
123
+ },
124
+ { orderBy: { createdAt: "DESC" } }
125
+ );
126
+ const latestBalanceAfter = latest ? fromUnits(toUnits(latest.balanceAfter)) : "0.0000";
127
+ const reconciled = toUnits(cachedBalance) === toUnits(sumOfAmounts) && (latest === null || toUnits(cachedBalance) === toUnits(latestBalanceAfter));
128
+ return { cachedBalance, sumOfAmounts, latestBalanceAfter, reconciled };
129
+ }
130
+ export {
131
+ PREPAID_USAGE_SENTINEL_RUN_ID,
132
+ applyBalanceMovement,
133
+ ensureBalanceRow,
134
+ reconcileAccountBalance
135
+ };
136
+ //# sourceMappingURL=balanceLedger.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/billing/lib/balanceLedger.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport {\n BillingAccountBalance,\n BillingAccountTransaction,\n} from '../data/entities'\nimport type { TransactionType } from '../data/validators'\nimport { fromUnits, subMoney, toUnits } from './money'\n\n/**\n * Prepaid balance ledger (SPEC-002).\n *\n * Two append-only primitives back every balance change:\n * - `BillingAccountBalance` \u2014 the O(1) running-balance cache, one row per\n * prepaid account; the row the atomic `UPDATE \u2026 RETURNING` locks.\n * - `BillingAccountTransaction` \u2014 the source of truth; `balance == SUM(amount)`.\n *\n * Concurrency correctness: the single-statement `UPDATE \u2026 RETURNING`\n * serializes via the Postgres row lock on the balance row, so N concurrent\n * uploads apply N debits with no lost update and each gets a distinct\n * `balance_after`. The transaction insert in the SAME DB transaction carries\n * that exact snapshot.\n *\n * **All functions here MUST be called inside an `em.transactional(...)`** so\n * the balance move and the transaction insert commit (or roll back) together.\n */\n\n// Sentinel `rated_in_bill_run_id` stamped on a usage record the moment it is\n// rated in real time, so a postpaid-style Bill Run never re-rates it. It is a\n// fixed, recognizable UUID (not a real run id) \u2014 its presence simply means\n// \"already rated at ingest, paired with a BillingAccountTransaction\".\nexport const PREPAID_USAGE_SENTINEL_RUN_ID = 'dddddddd-dddd-4ddd-8ddd-dddddddddddd'\n\nexport type BalanceRowInit = {\n organizationId: string\n tenantId: string\n billAccountId: string\n currencyCode: string\n lowBalanceThreshold?: string | null\n}\n\n/**\n * Find-or-create the 1:1 balance row for an account. Idempotent \u2014 safe to\n * call on every prepaid account create. Uses the ORM (this runs at account\n * create, not on the hot consume path).\n */\nexport async function ensureBalanceRow(\n em: EntityManager,\n init: BalanceRowInit,\n): Promise<BillingAccountBalance> {\n const existing = await em.findOne(BillingAccountBalance, {\n tenantId: init.tenantId,\n billAccountId: init.billAccountId,\n })\n if (existing) {\n // Allow create-time threshold to land even if a prior (defensive) row\n // already exists with no threshold set.\n if (\n init.lowBalanceThreshold !== undefined &&\n init.lowBalanceThreshold !== null &&\n (existing.lowBalanceThreshold === null || existing.lowBalanceThreshold === undefined)\n ) {\n existing.lowBalanceThreshold = init.lowBalanceThreshold\n existing.updatedAt = new Date()\n }\n return existing\n }\n const now = new Date()\n const row = em.create(BillingAccountBalance, {\n organizationId: init.organizationId,\n tenantId: init.tenantId,\n billAccountId: init.billAccountId,\n currencyCode: init.currencyCode,\n balance: '0',\n lowBalanceThreshold: init.lowBalanceThreshold ?? null,\n lastMovementAt: null,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(row)\n await em.flush()\n return row\n}\n\nexport type ApplyMovementParams = {\n organizationId: string\n tenantId: string\n billAccountId: string\n currencyCode: string\n /** Transaction type. */\n type: TransactionType\n /** SIGNED amount: > 0 credit, < 0 debit. */\n amount: string\n description: string\n usageId?: string | null\n billingItemId?: string | null\n topupId?: string | null\n statementId?: string | null\n /** Idempotency key \u2014 when set, a duplicate is a no-op (returns the prior row). */\n sourceRef?: string | null\n metadata?: Record<string, unknown> | null\n userId?: string | null\n}\n\nexport type ApplyMovementResult = {\n /** Running balance BEFORE this movement (4dp string). */\n balanceBefore: string\n /** Running balance AFTER this movement \u2014 authoritative (4dp string). */\n balanceAfter: string\n transaction: BillingAccountTransaction\n /** True when `sourceRef` already existed \u2014 no balance was moved. */\n deduplicated: boolean\n}\n\n/**\n * Atomically move the balance and append the paired transaction.\n *\n * Idempotent on `sourceRef`: if a transaction with the same\n * `(tenant_id, bill_account_id, source_ref)` already exists, NO balance move\n * happens and the existing row is returned (`deduplicated: true`). The unique\n * constraint is the correctness backstop if a race slips past the pre-check \u2014\n * the duplicate insert fails and rolls back the whole (caller's) transaction,\n * so the balance move is undone too. Never double-moves.\n */\nexport async function applyBalanceMovement(\n em: EntityManager,\n params: ApplyMovementParams,\n): Promise<ApplyMovementResult> {\n if (params.sourceRef) {\n const existing = await em.findOne(BillingAccountTransaction, {\n tenantId: params.tenantId,\n billAccountId: params.billAccountId,\n sourceRef: params.sourceRef,\n deletedAt: null,\n })\n if (existing) {\n return {\n balanceBefore: subMoney(existing.balanceAfter, existing.amount),\n balanceAfter: existing.balanceAfter,\n transaction: existing,\n deduplicated: true,\n }\n }\n }\n\n const now = new Date()\n // Self-healing: ensure the balance row exists (race-safe via the unique\n // constraint), then the atomic single-statement move. `amount` is signed\n // so one statement covers both credit and debit.\n await em.execute(\n `INSERT INTO billing_account_balances\n (id, organization_id, tenant_id, bill_account_id, currency_code, balance, created_at, updated_at)\n VALUES (gen_random_uuid(), ?, ?, ?, ?, 0, ?, ?)\n ON CONFLICT (tenant_id, bill_account_id) DO NOTHING`,\n [\n params.organizationId,\n params.tenantId,\n params.billAccountId,\n params.currencyCode,\n now,\n now,\n ],\n )\n const rows = (await em.execute(\n `UPDATE billing_account_balances\n SET balance = balance + ?, last_movement_at = ?, updated_at = ?\n WHERE tenant_id = ? AND bill_account_id = ?\n RETURNING balance`,\n [params.amount, now, now, params.tenantId, params.billAccountId],\n )) as unknown as Array<{ balance: string }>\n\n if (!rows || rows.length === 0) {\n // Should be unreachable \u2014 the upsert above guarantees a row exists.\n throw new Error(\n `balanceLedger: balance row missing for account ${params.billAccountId} after upsert`,\n )\n }\n // Normalize whatever the driver hands back (string or number) to 4dp.\n const balanceAfter = fromUnits(toUnits(rows[0].balance))\n const balanceBefore = subMoney(balanceAfter, params.amount)\n\n const transaction = em.create(BillingAccountTransaction, {\n organizationId: params.organizationId,\n tenantId: params.tenantId,\n billAccountId: params.billAccountId,\n type: params.type,\n amount: params.amount,\n currencyCode: params.currencyCode,\n balanceAfter,\n usageId: params.usageId ?? null,\n billingItemId: params.billingItemId ?? null,\n topupId: params.topupId ?? null,\n statementId: params.statementId ?? null,\n description: params.description,\n sourceRef: params.sourceRef ?? null,\n metadata: params.metadata ?? null,\n userId: params.userId ?? null,\n createdAt: now,\n })\n em.persist(transaction)\n await em.flush()\n\n return { balanceBefore, balanceAfter, transaction, deduplicated: false }\n}\n\nexport type ReconcileResult = {\n /** The denormalized running balance (cache). */\n cachedBalance: string\n /** SUM(amount) over all (non-deleted) transactions. */\n sumOfAmounts: string\n /** The most recent transaction's balance_after, or '0.0000' if none. */\n latestBalanceAfter: string\n /** True when all three agree. */\n reconciled: boolean\n}\n\n/**\n * Assert the ledger invariant for one account:\n * `cachedBalance == SUM(amount) == latest.balance_after`.\n *\n * The optional reconciliation worker (P5) runs this per account and alerts on\n * drift \u2014 a cheap guard against any future non-atomic write slipping in.\n */\nexport async function reconcileAccountBalance(\n em: EntityManager,\n scope: { tenantId: string; organizationId: string; billAccountId: string },\n): Promise<ReconcileResult> {\n const balanceRow = await em.findOne(BillingAccountBalance, {\n tenantId: scope.tenantId,\n billAccountId: scope.billAccountId,\n })\n const cachedBalance = balanceRow ? fromUnits(toUnits(balanceRow.balance)) : '0.0000'\n\n const sumRows = (await em.execute(\n `SELECT COALESCE(SUM(amount), 0) AS sum_amount\n FROM billing_account_transactions\n WHERE tenant_id = ? AND organization_id = ? AND bill_account_id = ? AND deleted_at IS NULL`,\n [scope.tenantId, scope.organizationId, scope.billAccountId],\n )) as unknown as Array<{ sum_amount: string }>\n const sumOfAmounts = fromUnits(toUnits(sumRows?.[0]?.sum_amount ?? 0))\n\n const latest = await em.findOne(\n BillingAccountTransaction,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n billAccountId: scope.billAccountId,\n deletedAt: null,\n },\n { orderBy: { createdAt: 'DESC' } },\n )\n const latestBalanceAfter = latest ? fromUnits(toUnits(latest.balanceAfter)) : '0.0000'\n\n const reconciled =\n toUnits(cachedBalance) === toUnits(sumOfAmounts) &&\n (latest === null || toUnits(cachedBalance) === toUnits(latestBalanceAfter))\n\n return { cachedBalance, sumOfAmounts, latestBalanceAfter, reconciled }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,29 @@
1
+ import { compareMoney, toUnits } from "./money.js";
2
+ function resolveLowBalanceThreshold(accountThreshold, configDefault) {
3
+ if (accountThreshold !== null && accountThreshold !== void 0) {
4
+ return accountThreshold;
5
+ }
6
+ if (configDefault !== null && configDefault !== void 0) {
7
+ return String(configDefault);
8
+ }
9
+ return "0";
10
+ }
11
+ function computeBalanceStatus(balance, threshold) {
12
+ if (compareMoney(balance, 0) <= 0) return "exhausted";
13
+ if (compareMoney(balance, threshold) <= 0) return "low";
14
+ return "ok";
15
+ }
16
+ function detectBalanceCrossing(balanceBefore, balanceAfter, threshold) {
17
+ const beforeUnits = toUnits(balanceBefore);
18
+ const afterUnits = toUnits(balanceAfter);
19
+ const thresholdUnits = toUnits(threshold);
20
+ const exhaustedCrossedDown = beforeUnits > 0 && afterUnits <= 0;
21
+ const lowCrossedDown = thresholdUnits > 0 && beforeUnits > thresholdUnits && afterUnits <= thresholdUnits && afterUnits > 0;
22
+ return { lowCrossedDown, exhaustedCrossedDown };
23
+ }
24
+ export {
25
+ computeBalanceStatus,
26
+ detectBalanceCrossing,
27
+ resolveLowBalanceThreshold
28
+ };
29
+ //# sourceMappingURL=balanceStatus.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/billing/lib/balanceStatus.ts"],
4
+ "sourcesContent": ["import type { BalanceStatus } from '../data/validators'\nimport { compareMoney, toUnits } from './money'\n\n/**\n * Prepaid funds status & threshold-crossing detection (SPEC-002).\n *\n * Usage is NEVER rejected \u2014 we only classify and report. Events fire ONLY on\n * a downward crossing (compare `balance_before` vs `balance_after`), so a\n * client sitting at `exhausted` and still pushing usage is not spammed.\n */\n\n/**\n * Resolve the effective low-balance threshold:\n * account row's own threshold \u2192 tenant config default \u2192 0 (off).\n */\nexport function resolveLowBalanceThreshold(\n accountThreshold: string | null | undefined,\n configDefault: string | number | null | undefined,\n): string {\n if (accountThreshold !== null && accountThreshold !== undefined) {\n return accountThreshold\n }\n if (configDefault !== null && configDefault !== undefined) {\n return String(configDefault)\n }\n return '0'\n}\n\n/**\n * | status | condition |\n * |------------|------------------------------------|\n * | exhausted | balance <= 0 (incl. negative) |\n * | low | 0 < balance <= threshold |\n * | ok | balance > threshold |\n */\nexport function computeBalanceStatus(\n balance: string | number,\n threshold: string | number,\n): BalanceStatus {\n if (compareMoney(balance, 0) <= 0) return 'exhausted'\n if (compareMoney(balance, threshold) <= 0) return 'low'\n return 'ok'\n}\n\nexport type BalanceCrossing = {\n /** Crossed downward from > threshold into the (0, threshold] low band. */\n lowCrossedDown: boolean\n /** Crossed downward from > 0 to <= 0. */\n exhaustedCrossedDown: boolean\n}\n\n/**\n * Detect downward threshold crossings between two balances.\n *\n * A single large debit that jumps straight past the low band into exhausted\n * fires ONLY `exhausted` (the customer is already negative \u2014 \"you're low\" is\n * moot). `low` fires only when the balance lands in the (0, threshold] band.\n * When `threshold <= 0` the low band is empty, so `low` never fires (the\n * config default 0 = \"off\"); `exhausted` still works.\n */\nexport function detectBalanceCrossing(\n balanceBefore: string | number,\n balanceAfter: string | number,\n threshold: string | number,\n): BalanceCrossing {\n const beforeUnits = toUnits(balanceBefore)\n const afterUnits = toUnits(balanceAfter)\n const thresholdUnits = toUnits(threshold)\n\n const exhaustedCrossedDown = beforeUnits > 0 && afterUnits <= 0\n const lowCrossedDown =\n thresholdUnits > 0 &&\n beforeUnits > thresholdUnits &&\n afterUnits <= thresholdUnits &&\n afterUnits > 0\n\n return { lowCrossedDown, exhaustedCrossedDown }\n}\n"],
5
+ "mappings": "AACA,SAAS,cAAc,eAAe;AAc/B,SAAS,2BACd,kBACA,eACQ;AACR,MAAI,qBAAqB,QAAQ,qBAAqB,QAAW;AAC/D,WAAO;AAAA,EACT;AACA,MAAI,kBAAkB,QAAQ,kBAAkB,QAAW;AACzD,WAAO,OAAO,aAAa;AAAA,EAC7B;AACA,SAAO;AACT;AASO,SAAS,qBACd,SACA,WACe;AACf,MAAI,aAAa,SAAS,CAAC,KAAK,EAAG,QAAO;AAC1C,MAAI,aAAa,SAAS,SAAS,KAAK,EAAG,QAAO;AAClD,SAAO;AACT;AAkBO,SAAS,sBACd,eACA,cACA,WACiB;AACjB,QAAM,cAAc,QAAQ,aAAa;AACzC,QAAM,aAAa,QAAQ,YAAY;AACvC,QAAM,iBAAiB,QAAQ,SAAS;AAExC,QAAM,uBAAuB,cAAc,KAAK,cAAc;AAC9D,QAAM,iBACJ,iBAAiB,KACjB,cAAc,kBACd,cAAc,kBACd,aAAa;AAEf,SAAO,EAAE,gBAAgB,qBAAqB;AAChD;",
6
+ "names": []
7
+ }