@dainamite/billing 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/dist/modules/billing/acl.js +13 -1
- package/dist/modules/billing/acl.js.map +2 -2
- package/dist/modules/billing/api/accounts/[id]/summary/route.js +52 -0
- package/dist/modules/billing/api/accounts/[id]/summary/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +47 -4
- package/dist/modules/billing/api/accounts/route.js.map +2 -2
- package/dist/modules/billing/api/export/account/[id]/route.js +11 -7
- package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/[id]/memos/route.js +54 -0
- package/dist/modules/billing/api/invoices/[id]/memos/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/pdf/route.js +70 -0
- package/dist/modules/billing/api/invoices/[id]/pdf/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/route.js +44 -24
- package/dist/modules/billing/api/invoices/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/[id]/send/route.js +107 -0
- package/dist/modules/billing/api/invoices/[id]/send/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/add-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/add-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/delete-draft/route.js +33 -0
- package/dist/modules/billing/api/invoices/delete-draft/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/edit-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/post/route.js +2 -2
- package/dist/modules/billing/api/invoices/post/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/remove-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/remove-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/route.js +45 -46
- package/dist/modules/billing/api/invoices/route.js.map +2 -2
- package/dist/modules/billing/api/items/bulk/route.js +1 -1
- package/dist/modules/billing/api/items/bulk/route.js.map +2 -2
- package/dist/modules/billing/api/items/change-price/route.js +44 -0
- package/dist/modules/billing/api/items/change-price/route.js.map +7 -0
- package/dist/modules/billing/api/items/route.js +19 -7
- package/dist/modules/billing/api/items/route.js.map +2 -2
- package/dist/modules/billing/api/payment-imports/[id]/route.js +49 -0
- package/dist/modules/billing/api/payment-imports/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/payment-imports/route.js +57 -0
- package/dist/modules/billing/api/payment-imports/route.js.map +7 -0
- package/dist/modules/billing/api/payments/[id]/route.js +60 -0
- package/dist/modules/billing/api/payments/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/payments/allocate/route.js +35 -0
- package/dist/modules/billing/api/payments/allocate/route.js.map +7 -0
- package/dist/modules/billing/api/payments/assign/route.js +36 -0
- package/dist/modules/billing/api/payments/assign/route.js.map +7 -0
- package/dist/modules/billing/api/payments/convert-to-topup/route.js +35 -0
- package/dist/modules/billing/api/payments/convert-to-topup/route.js.map +7 -0
- package/dist/modules/billing/api/payments/ignore/route.js +35 -0
- package/dist/modules/billing/api/payments/ignore/route.js.map +7 -0
- package/dist/modules/billing/api/payments/import/route.js +63 -0
- package/dist/modules/billing/api/payments/import/route.js.map +7 -0
- package/dist/modules/billing/api/payments/restore/route.js +35 -0
- package/dist/modules/billing/api/payments/restore/route.js.map +7 -0
- package/dist/modules/billing/api/payments/route.js +114 -0
- package/dist/modules/billing/api/payments/route.js.map +7 -0
- package/dist/modules/billing/api/payments/unallocate/route.js +35 -0
- package/dist/modules/billing/api/payments/unallocate/route.js.map +7 -0
- package/dist/modules/billing/api/payments/unassign/route.js +35 -0
- package/dist/modules/billing/api/payments/unassign/route.js.map +7 -0
- package/dist/modules/billing/api/run-outcomes/route.js +43 -6
- package/dist/modules/billing/api/run-outcomes/route.js.map +2 -2
- package/dist/modules/billing/api/runs/[id]/post-drafts/route.js +61 -0
- package/dist/modules/billing/api/runs/[id]/post-drafts/route.js.map +7 -0
- package/dist/modules/billing/api/runs/[id]/route.js +53 -0
- package/dist/modules/billing/api/runs/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js +1 -1
- package/dist/modules/billing/api/runs/retry-failed/route.js.map +2 -2
- package/dist/modules/billing/api/runs/route.js +9 -3
- package/dist/modules/billing/api/runs/route.js.map +2 -2
- package/dist/modules/billing/api/settings/number-preview/route.js +42 -0
- package/dist/modules/billing/api/settings/number-preview/route.js.map +7 -0
- package/dist/modules/billing/api/settings/route.js +92 -0
- package/dist/modules/billing/api/settings/route.js.map +7 -0
- package/dist/modules/billing/api/statements/route.js +3 -0
- package/dist/modules/billing/api/statements/route.js.map +2 -2
- package/dist/modules/billing/api/test-invoices/wipe/route.js +2 -2
- package/dist/modules/billing/api/test-invoices/wipe/route.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +121 -232
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/create/page.js +6 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/page.js +63 -4
- package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js +297 -65
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +3 -3
- package/dist/modules/billing/backend/billing/invoices/page.js +6 -172
- package/dist/modules/billing/backend/billing/invoices/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/[id]/page.js +51 -7
- package/dist/modules/billing/backend/billing/items/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/create/page.js +9 -6
- package/dist/modules/billing/backend/billing/items/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/page.js +8 -2
- package/dist/modules/billing/backend/billing/items/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/payments/[id]/page.js +297 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js +74 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/page.js +58 -0
- package/dist/modules/billing/backend/billing/payments/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/payments/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js +49 -14
- package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/runs/page.js +58 -7
- package/dist/modules/billing/backend/billing/runs/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/[id]/page.js +61 -6
- package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/page.js +1 -1
- package/dist/modules/billing/backend/billing/statements/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/topups/page.js +1 -1
- package/dist/modules/billing/backend/billing/topups/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/transactions/page.js +1 -1
- package/dist/modules/billing/backend/billing/transactions/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/usage/create/page.js +3 -2
- package/dist/modules/billing/backend/billing/usage/create/page.js.map +2 -2
- package/dist/modules/billing/backend/config/billing/page.js +472 -0
- package/dist/modules/billing/backend/config/billing/page.js.map +7 -0
- package/dist/modules/billing/backend/config/billing/page.meta.js +19 -0
- package/dist/modules/billing/backend/config/billing/page.meta.js.map +7 -0
- package/dist/modules/billing/cli.js +22 -1
- package/dist/modules/billing/cli.js.map +2 -2
- package/dist/modules/billing/commands/accounts.js +6 -0
- package/dist/modules/billing/commands/accounts.js.map +2 -2
- package/dist/modules/billing/commands/index.js +3 -0
- package/dist/modules/billing/commands/index.js.map +2 -2
- package/dist/modules/billing/commands/invoices.js +269 -167
- package/dist/modules/billing/commands/invoices.js.map +2 -2
- package/dist/modules/billing/commands/items.js +108 -0
- package/dist/modules/billing/commands/items.js.map +2 -2
- package/dist/modules/billing/commands/memos.js +179 -0
- package/dist/modules/billing/commands/memos.js.map +7 -0
- package/dist/modules/billing/commands/payments.js +379 -0
- package/dist/modules/billing/commands/payments.js.map +7 -0
- package/dist/modules/billing/commands/runs.js +151 -4
- package/dist/modules/billing/commands/runs.js.map +2 -2
- package/dist/modules/billing/commands/settings.js +59 -0
- package/dist/modules/billing/commands/settings.js.map +7 -0
- package/dist/modules/billing/commands/topups.js +9 -2
- package/dist/modules/billing/commands/topups.js.map +2 -2
- package/dist/modules/billing/commands/transactions.js +2 -2
- package/dist/modules/billing/commands/transactions.js.map +2 -2
- package/dist/modules/billing/components/AccountForm.js +149 -42
- package/dist/modules/billing/components/AccountForm.js.map +2 -2
- package/dist/modules/billing/components/AccountInvoicesSection.js +64 -0
- package/dist/modules/billing/components/AccountInvoicesSection.js.map +7 -0
- package/dist/modules/billing/components/AccountPaymentsSection.js +43 -0
- package/dist/modules/billing/components/AccountPaymentsSection.js.map +7 -0
- package/dist/modules/billing/components/AccountPicker.js +56 -0
- package/dist/modules/billing/components/AccountPicker.js.map +7 -0
- package/dist/modules/billing/components/AllocatePaymentDialog.js +134 -0
- package/dist/modules/billing/components/AllocatePaymentDialog.js.map +7 -0
- package/dist/modules/billing/components/BillingDataTable.js +21 -0
- package/dist/modules/billing/components/BillingDataTable.js.map +7 -0
- package/dist/modules/billing/components/BillingSettingsLink.js +19 -0
- package/dist/modules/billing/components/BillingSettingsLink.js.map +7 -0
- package/dist/modules/billing/components/ChangeItemPriceDialog.js +158 -0
- package/dist/modules/billing/components/ChangeItemPriceDialog.js.map +7 -0
- package/dist/modules/billing/components/CreateMemoDialog.js +200 -0
- package/dist/modules/billing/components/CreateMemoDialog.js.map +7 -0
- package/dist/modules/billing/components/CustomerLabel.js +33 -0
- package/dist/modules/billing/components/CustomerLabel.js.map +7 -0
- package/dist/modules/billing/components/InvoicesTable.js +270 -0
- package/dist/modules/billing/components/InvoicesTable.js.map +7 -0
- package/dist/modules/billing/components/ItemForm.js +176 -44
- package/dist/modules/billing/components/ItemForm.js.map +2 -2
- package/dist/modules/billing/components/ItemPriceField.js +46 -0
- package/dist/modules/billing/components/ItemPriceField.js.map +7 -0
- package/dist/modules/billing/components/LineFormDialog.js +25 -2
- package/dist/modules/billing/components/LineFormDialog.js.map +2 -2
- package/dist/modules/billing/components/ManualPaymentDialog.js +122 -0
- package/dist/modules/billing/components/ManualPaymentDialog.js.map +7 -0
- package/dist/modules/billing/components/MoneyInput.js +14 -0
- package/dist/modules/billing/components/MoneyInput.js.map +7 -0
- package/dist/modules/billing/components/PaymentUploadDialog.js +115 -0
- package/dist/modules/billing/components/PaymentUploadDialog.js.map +7 -0
- package/dist/modules/billing/components/PaymentsTable.js +171 -0
- package/dist/modules/billing/components/PaymentsTable.js.map +7 -0
- package/dist/modules/billing/components/PostRunDraftsButton.js +105 -0
- package/dist/modules/billing/components/PostRunDraftsButton.js.map +7 -0
- package/dist/modules/billing/components/PrepaidPanel.js +14 -4
- package/dist/modules/billing/components/PrepaidPanel.js.map +2 -2
- package/dist/modules/billing/components/RunCyclesSection.js +209 -0
- package/dist/modules/billing/components/RunCyclesSection.js.map +7 -0
- package/dist/modules/billing/components/TestRunActions.js +127 -0
- package/dist/modules/billing/components/TestRunActions.js.map +7 -0
- package/dist/modules/billing/components/account/AccountItemsTab.js +157 -0
- package/dist/modules/billing/components/account/AccountItemsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountRunBillingDialog.js +122 -0
- package/dist/modules/billing/components/account/AccountRunBillingDialog.js.map +7 -0
- package/dist/modules/billing/components/account/AccountRunsTab.js +152 -0
- package/dist/modules/billing/components/account/AccountRunsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountStatementsTab.js +98 -0
- package/dist/modules/billing/components/account/AccountStatementsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountSummaryCards.js +125 -0
- package/dist/modules/billing/components/account/AccountSummaryCards.js.map +7 -0
- package/dist/modules/billing/components/account/AccountUsageTab.js +91 -0
- package/dist/modules/billing/components/account/AccountUsageTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountView.js +73 -0
- package/dist/modules/billing/components/account/AccountView.js.map +7 -0
- package/dist/modules/billing/components/account/accountRow.js +39 -0
- package/dist/modules/billing/components/account/accountRow.js.map +7 -0
- package/dist/modules/billing/components/customerNames.js +42 -0
- package/dist/modules/billing/components/customerNames.js.map +7 -0
- package/dist/modules/billing/data/entities.js +560 -4
- package/dist/modules/billing/data/entities.js.map +2 -2
- package/dist/modules/billing/data/entityIds.js +10 -1
- package/dist/modules/billing/data/entityIds.js.map +2 -2
- package/dist/modules/billing/data/validators.js +269 -15
- package/dist/modules/billing/data/validators.js.map +2 -2
- package/dist/modules/billing/events.js +50 -0
- package/dist/modules/billing/events.js.map +2 -2
- package/dist/modules/billing/i18n/de.json +880 -0
- package/dist/modules/billing/i18n/en.json +457 -21
- package/dist/modules/billing/i18n/es.json +880 -0
- package/dist/modules/billing/i18n/pl.json +458 -22
- package/dist/modules/billing/index.js +6 -2
- package/dist/modules/billing/index.js.map +2 -2
- package/dist/modules/billing/lib/accountFinancials.js +58 -0
- package/dist/modules/billing/lib/accountFinancials.js.map +7 -0
- package/dist/modules/billing/lib/accountLock.js +14 -0
- package/dist/modules/billing/lib/accountLock.js.map +7 -0
- package/dist/modules/billing/lib/accountStatus.js +9 -7
- package/dist/modules/billing/lib/accountStatus.js.map +2 -2
- package/dist/modules/billing/lib/accountSummary.js +106 -0
- package/dist/modules/billing/lib/accountSummary.js.map +7 -0
- package/dist/modules/billing/lib/allocation.js +356 -0
- package/dist/modules/billing/lib/allocation.js.map +7 -0
- package/dist/modules/billing/lib/allocationEvents.js +22 -0
- package/dist/modules/billing/lib/allocationEvents.js.map +7 -0
- package/dist/modules/billing/lib/billPeriod.js +15 -1
- package/dist/modules/billing/lib/billPeriod.js.map +2 -2
- package/dist/modules/billing/lib/billRunEngine.js +237 -102
- package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
- package/dist/modules/billing/lib/billingSettings.js +116 -0
- package/dist/modules/billing/lib/billingSettings.js.map +7 -0
- package/dist/modules/billing/lib/commandRoute.js +81 -0
- package/dist/modules/billing/lib/commandRoute.js.map +7 -0
- package/dist/modules/billing/lib/customerLookup.js +97 -0
- package/dist/modules/billing/lib/customerLookup.js.map +7 -0
- package/dist/modules/billing/lib/dateOnly.js +13 -0
- package/dist/modules/billing/lib/dateOnly.js.map +7 -0
- package/dist/modules/billing/lib/documentParties.js +16 -0
- package/dist/modules/billing/lib/documentParties.js.map +7 -0
- package/dist/modules/billing/lib/documentTypes.js +27 -0
- package/dist/modules/billing/lib/documentTypes.js.map +7 -0
- package/dist/modules/billing/lib/dueDate.js +18 -0
- package/dist/modules/billing/lib/dueDate.js.map +7 -0
- package/dist/modules/billing/lib/iban.js +29 -0
- package/dist/modules/billing/lib/iban.js.map +7 -0
- package/dist/modules/billing/lib/invoiceDelivery.js +133 -0
- package/dist/modules/billing/lib/invoiceDelivery.js.map +7 -0
- package/dist/modules/billing/lib/invoiceLanguages.js +28 -0
- package/dist/modules/billing/lib/invoiceLanguages.js.map +7 -0
- package/dist/modules/billing/lib/invoiceWriter.js +107 -99
- package/dist/modules/billing/lib/invoiceWriter.js.map +2 -2
- package/dist/modules/billing/lib/itemBillingState.js +27 -0
- package/dist/modules/billing/lib/itemBillingState.js.map +7 -0
- package/dist/modules/billing/lib/itemPriceChange.js +18 -0
- package/dist/modules/billing/lib/itemPriceChange.js.map +7 -0
- package/dist/modules/billing/lib/itemSelector.js.map +2 -2
- package/dist/modules/billing/lib/lineAmounts.js +32 -0
- package/dist/modules/billing/lib/lineAmounts.js.map +7 -0
- package/dist/modules/billing/lib/memoGuards.js +25 -0
- package/dist/modules/billing/lib/memoGuards.js.map +7 -0
- package/dist/modules/billing/lib/navigation.js +48 -0
- package/dist/modules/billing/lib/navigation.js.map +7 -0
- package/dist/modules/billing/lib/numbering.js +120 -0
- package/dist/modules/billing/lib/numbering.js.map +7 -0
- package/dist/modules/billing/lib/overdue.js +36 -0
- package/dist/modules/billing/lib/overdue.js.map +7 -0
- package/dist/modules/billing/lib/paymentCsv.js +125 -0
- package/dist/modules/billing/lib/paymentCsv.js.map +7 -0
- package/dist/modules/billing/lib/paymentGateway.js +15 -0
- package/dist/modules/billing/lib/paymentGateway.js.map +7 -0
- package/dist/modules/billing/lib/paymentImport.js +164 -0
- package/dist/modules/billing/lib/paymentImport.js.map +7 -0
- package/dist/modules/billing/lib/paymentMatcher.js +36 -0
- package/dist/modules/billing/lib/paymentMatcher.js.map +7 -0
- package/dist/modules/billing/lib/pdf/attachmentStore.js +46 -0
- package/dist/modules/billing/lib/pdf/attachmentStore.js.map +7 -0
- package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js +7 -0
- package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoiceModel.js +194 -0
- package/dist/modules/billing/lib/pdf/invoiceModel.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoicePdf.js +165 -0
- package/dist/modules/billing/lib/pdf/invoicePdf.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoiceTemplate.js +276 -0
- package/dist/modules/billing/lib/pdf/invoiceTemplate.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConfig.js +17 -25
- package/dist/modules/billing/lib/prepaidConfig.js.map +2 -2
- package/dist/modules/billing/lib/prepaidConsume.js +26 -7
- package/dist/modules/billing/lib/prepaidConsume.js.map +2 -2
- package/dist/modules/billing/lib/prepaidPeriodClose.js +277 -158
- package/dist/modules/billing/lib/prepaidPeriodClose.js.map +2 -2
- package/dist/modules/billing/lib/prepaidTaxPoint.js +37 -0
- package/dist/modules/billing/lib/prepaidTaxPoint.js.map +7 -0
- package/dist/modules/billing/lib/seeds/demo.js +51 -3
- package/dist/modules/billing/lib/seeds/demo.js.map +2 -2
- package/dist/modules/billing/lib/seeds/paymentsSample.js +93 -0
- package/dist/modules/billing/lib/seeds/paymentsSample.js.map +7 -0
- package/dist/modules/billing/lib/seeds/sellerSettings.js +41 -0
- package/dist/modules/billing/lib/seeds/sellerSettings.js.map +7 -0
- package/dist/modules/billing/lib/servicePeriods.js +121 -0
- package/dist/modules/billing/lib/servicePeriods.js.map +7 -0
- package/dist/modules/billing/lib/testRunCleanup.js +24 -0
- package/dist/modules/billing/lib/testRunCleanup.js.map +7 -0
- package/dist/modules/billing/lib/topupCapture.js +2 -2
- package/dist/modules/billing/lib/topupCapture.js.map +2 -2
- package/dist/modules/billing/lib/topupReceipt.js +58 -79
- package/dist/modules/billing/lib/topupReceipt.js.map +2 -2
- package/dist/modules/billing/lib/usageRater.js.map +1 -1
- package/dist/modules/billing/lib/usageRunner.js +14 -6
- package/dist/modules/billing/lib/usageRunner.js.map +2 -2
- package/dist/modules/billing/lib/vat.js +39 -0
- package/dist/modules/billing/lib/vat.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js +15 -0
- package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js +25 -0
- package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js +115 -0
- package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js +47 -0
- package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js +89 -0
- package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js +17 -0
- package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js +27 -0
- package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js +29 -0
- package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js.map +7 -0
- package/dist/modules/billing/setup.js +7 -184
- package/dist/modules/billing/setup.js.map +2 -2
- package/dist/modules/billing/subscribers/credit-memo-auto-send.js +16 -0
- package/dist/modules/billing/subscribers/credit-memo-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/debit-memo-apply-credit.js +16 -0
- package/dist/modules/billing/subscribers/debit-memo-apply-credit.js.map +7 -0
- package/dist/modules/billing/subscribers/debit-memo-auto-send.js +16 -0
- package/dist/modules/billing/subscribers/debit-memo-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/invoice-apply-credit.js +34 -0
- package/dist/modules/billing/subscribers/invoice-apply-credit.js.map +7 -0
- package/dist/modules/billing/subscribers/invoice-auto-send.js +30 -0
- package/dist/modules/billing/subscribers/invoice-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js +69 -33
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +2 -2
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js +4 -6
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +2 -2
- package/generated/entities/billing_account/index.ts +3 -0
- package/generated/entities/billing_allocation/index.ts +18 -0
- package/generated/entities/billing_document_sequence/index.ts +11 -0
- package/generated/entities/billing_invoice/index.ts +38 -0
- package/generated/entities/billing_invoice_line/index.ts +24 -0
- package/generated/entities/billing_item/index.ts +4 -0
- package/generated/entities/billing_payment/index.ts +25 -0
- package/generated/entities/billing_payment_import/index.ts +14 -0
- package/generated/entities/billing_run/index.ts +0 -1
- package/generated/entities/billing_run_outcome/index.ts +1 -0
- package/generated/entities/billing_settings/index.ts +26 -0
- package/generated/entities/billing_statement/index.ts +1 -0
- package/generated/entities.ids.generated.ts +7 -0
- package/package.json +5 -1
- package/src/modules/billing/__integration__/TC-BILL-004.spec.ts +171 -0
- package/src/modules/billing/__integration__/TC-BILL-005.spec.ts +134 -0
- package/src/modules/billing/__integration__/TC-BILL-006.spec.ts +101 -0
- package/src/modules/billing/__integration__/TC-BILL-007.spec.ts +140 -0
- package/src/modules/billing/__integration__/TC-BILL-008.spec.ts +108 -0
- package/src/modules/billing/__integration__/TC-BILL-009.spec.ts +113 -0
- package/src/modules/billing/__integration__/TC-BILL-010.spec.ts +118 -0
- package/src/modules/billing/__integration__/TC-BILL-011.spec.ts +142 -0
- package/src/modules/billing/__integration__/TC-BILL-012.spec.ts +115 -0
- package/src/modules/billing/__integration__/TC-BILL-013.spec.ts +118 -0
- package/src/modules/billing/__integration__/sellerSettings.ts +24 -0
- package/src/modules/billing/__tests__/commands/invoices.test.ts +436 -354
- package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +33 -0
- package/src/modules/billing/__tests__/commands/memos.test.ts +205 -0
- package/src/modules/billing/__tests__/commands/payments.test.ts +241 -0
- package/src/modules/billing/__tests__/commands/runsDelete.test.ts +146 -0
- package/src/modules/billing/__tests__/commands/runsPostDrafts.test.ts +106 -0
- package/src/modules/billing/__tests__/commands/topups.test.ts +11 -0
- package/src/modules/billing/__tests__/entityRegistry.test.ts +8 -1
- package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +28 -11
- package/src/modules/billing/__tests__/i18n/dictionaries.test.ts +64 -0
- package/src/modules/billing/__tests__/lib/accountFinancials.test.ts +53 -0
- package/src/modules/billing/__tests__/lib/accountSummary.test.ts +97 -0
- package/src/modules/billing/__tests__/lib/allocation.test.ts +319 -0
- package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +228 -17
- package/src/modules/billing/__tests__/lib/billingSettings.test.ts +109 -0
- package/src/modules/billing/__tests__/lib/customerLookup.test.ts +156 -0
- package/src/modules/billing/__tests__/lib/dueDate.test.ts +35 -0
- package/src/modules/billing/__tests__/lib/iban.test.ts +38 -0
- package/src/modules/billing/__tests__/lib/invoiceDelivery.test.ts +206 -0
- package/src/modules/billing/__tests__/lib/invoiceLanguages.test.ts +49 -0
- package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +212 -10
- package/src/modules/billing/__tests__/lib/itemBillingState.test.ts +42 -0
- package/src/modules/billing/__tests__/lib/itemPriceChange.test.ts +165 -0
- package/src/modules/billing/__tests__/lib/lineAmounts.test.ts +86 -0
- package/src/modules/billing/__tests__/lib/navigation.test.ts +71 -0
- package/src/modules/billing/__tests__/lib/numbering.test.ts +91 -0
- package/src/modules/billing/__tests__/lib/overdue.test.ts +36 -0
- package/src/modules/billing/__tests__/lib/paymentCsv.test.ts +148 -0
- package/src/modules/billing/__tests__/lib/paymentImport.test.ts +113 -0
- package/src/modules/billing/__tests__/lib/paymentsSample.test.ts +73 -0
- package/src/modules/billing/__tests__/lib/pdf/invoicePdf.test.ts +269 -0
- package/src/modules/billing/__tests__/lib/pdf/invoicePdfService.test.ts +147 -0
- package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +374 -98
- package/src/modules/billing/__tests__/lib/prepaidTaxPoint.test.ts +118 -0
- package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +53 -2
- package/src/modules/billing/__tests__/lib/sellerSettings.test.ts +28 -0
- package/src/modules/billing/__tests__/lib/servicePeriods.test.ts +190 -0
- package/src/modules/billing/__tests__/lib/topupCapture.test.ts +6 -28
- package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +21 -52
- package/src/modules/billing/__tests__/lib/vat.test.ts +51 -0
- package/src/modules/billing/__tests__/package-isolation.test.ts +22 -8
- package/src/modules/billing/__tests__/setup.test.ts +13 -4
- package/src/modules/billing/__tests__/validators.test.ts +197 -3
- package/src/modules/billing/acl.ts +12 -0
- package/src/modules/billing/api/accounts/[id]/summary/route.ts +62 -0
- package/src/modules/billing/api/accounts/route.ts +59 -4
- package/src/modules/billing/api/export/account/[id]/route.ts +13 -11
- package/src/modules/billing/api/invoices/[id]/memos/route.ts +63 -0
- package/src/modules/billing/api/invoices/[id]/pdf/route.ts +80 -0
- package/src/modules/billing/api/invoices/[id]/route.ts +57 -70
- package/src/modules/billing/api/invoices/[id]/send/route.ts +127 -0
- package/src/modules/billing/api/invoices/add-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/delete-draft/route.ts +33 -0
- package/src/modules/billing/api/invoices/edit-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/post/route.ts +7 -6
- package/src/modules/billing/api/invoices/remove-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/route.ts +81 -86
- package/src/modules/billing/api/items/bulk/route.ts +2 -1
- package/src/modules/billing/api/items/change-price/route.ts +54 -0
- package/src/modules/billing/api/items/route.ts +23 -7
- package/src/modules/billing/api/payment-imports/[id]/route.ts +49 -0
- package/src/modules/billing/api/payment-imports/route.ts +57 -0
- package/src/modules/billing/api/payments/[id]/route.ts +63 -0
- package/src/modules/billing/api/payments/allocate/route.ts +35 -0
- package/src/modules/billing/api/payments/assign/route.ts +36 -0
- package/src/modules/billing/api/payments/convert-to-topup/route.ts +35 -0
- package/src/modules/billing/api/payments/ignore/route.ts +35 -0
- package/src/modules/billing/api/payments/import/route.ts +72 -0
- package/src/modules/billing/api/payments/restore/route.ts +35 -0
- package/src/modules/billing/api/payments/route.ts +147 -0
- package/src/modules/billing/api/payments/unallocate/route.ts +35 -0
- package/src/modules/billing/api/payments/unassign/route.ts +35 -0
- package/src/modules/billing/api/run-outcomes/route.ts +49 -6
- package/src/modules/billing/api/runs/[id]/post-drafts/route.ts +71 -0
- package/src/modules/billing/api/runs/[id]/route.ts +59 -0
- package/src/modules/billing/api/runs/retry-failed/route.ts +2 -1
- package/src/modules/billing/api/runs/route.ts +15 -7
- package/src/modules/billing/api/settings/number-preview/route.ts +46 -0
- package/src/modules/billing/api/settings/route.ts +116 -0
- package/src/modules/billing/api/statements/route.ts +5 -0
- package/src/modules/billing/api/test-invoices/wipe/route.ts +2 -2
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +151 -372
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +6 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +70 -4
- package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +444 -95
- package/src/modules/billing/backend/billing/invoices/page.tsx +8 -227
- package/src/modules/billing/backend/billing/items/[id]/page.tsx +75 -6
- package/src/modules/billing/backend/billing/items/create/page.tsx +13 -8
- package/src/modules/billing/backend/billing/items/page.tsx +18 -2
- package/src/modules/billing/backend/billing/payments/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/payments/[id]/page.tsx +415 -0
- package/src/modules/billing/backend/billing/payments/imports/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/payments/imports/[id]/page.tsx +106 -0
- package/src/modules/billing/backend/billing/payments/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/payments/page.tsx +56 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.tsx +49 -18
- package/src/modules/billing/backend/billing/runs/page.tsx +59 -8
- package/src/modules/billing/backend/billing/statements/[id]/page.tsx +86 -5
- package/src/modules/billing/backend/billing/statements/page.tsx +1 -1
- package/src/modules/billing/backend/billing/topups/page.tsx +1 -1
- package/src/modules/billing/backend/billing/transactions/page.tsx +1 -1
- package/src/modules/billing/backend/billing/usage/create/page.tsx +3 -2
- package/src/modules/billing/backend/config/billing/page.meta.ts +21 -0
- package/src/modules/billing/backend/config/billing/page.tsx +549 -0
- package/src/modules/billing/cli.ts +31 -1
- package/src/modules/billing/commands/accounts.ts +6 -0
- package/src/modules/billing/commands/index.ts +3 -0
- package/src/modules/billing/commands/invoices.ts +364 -231
- package/src/modules/billing/commands/items.ts +143 -0
- package/src/modules/billing/commands/memos.ts +218 -0
- package/src/modules/billing/commands/payments.ts +428 -0
- package/src/modules/billing/commands/runs.ts +205 -3
- package/src/modules/billing/commands/settings.ts +77 -0
- package/src/modules/billing/commands/topups.ts +12 -3
- package/src/modules/billing/commands/transactions.ts +2 -2
- package/src/modules/billing/components/AccountForm.tsx +199 -53
- package/src/modules/billing/components/AccountInvoicesSection.tsx +75 -0
- package/src/modules/billing/components/AccountPaymentsSection.tsx +42 -0
- package/src/modules/billing/components/AccountPicker.tsx +70 -0
- package/src/modules/billing/components/AllocatePaymentDialog.tsx +167 -0
- package/src/modules/billing/components/BillingDataTable.tsx +34 -0
- package/src/modules/billing/components/BillingSettingsLink.tsx +25 -0
- package/src/modules/billing/components/ChangeItemPriceDialog.tsx +180 -0
- package/src/modules/billing/components/CreateMemoDialog.tsx +232 -0
- package/src/modules/billing/components/CustomerLabel.tsx +53 -0
- package/src/modules/billing/components/InvoicesTable.tsx +344 -0
- package/src/modules/billing/components/ItemForm.tsx +233 -44
- package/src/modules/billing/components/ItemPriceField.tsx +81 -0
- package/src/modules/billing/components/LineFormDialog.tsx +25 -1
- package/src/modules/billing/components/ManualPaymentDialog.tsx +140 -0
- package/src/modules/billing/components/MoneyInput.tsx +26 -0
- package/src/modules/billing/components/PaymentUploadDialog.tsx +148 -0
- package/src/modules/billing/components/PaymentsTable.tsx +213 -0
- package/src/modules/billing/components/PostRunDraftsButton.tsx +129 -0
- package/src/modules/billing/components/PrepaidPanel.tsx +19 -6
- package/src/modules/billing/components/RunCyclesSection.tsx +298 -0
- package/src/modules/billing/components/TestRunActions.tsx +152 -0
- package/src/modules/billing/components/account/AccountItemsTab.tsx +192 -0
- package/src/modules/billing/components/account/AccountRunBillingDialog.tsx +147 -0
- package/src/modules/billing/components/account/AccountRunsTab.tsx +201 -0
- package/src/modules/billing/components/account/AccountStatementsTab.tsx +120 -0
- package/src/modules/billing/components/account/AccountSummaryCards.tsx +153 -0
- package/src/modules/billing/components/account/AccountUsageTab.tsx +122 -0
- package/src/modules/billing/components/account/AccountView.tsx +101 -0
- package/src/modules/billing/components/account/accountRow.ts +70 -0
- package/src/modules/billing/components/customerNames.ts +62 -0
- package/src/modules/billing/data/entities.ts +656 -6
- package/src/modules/billing/data/entityIds.ts +9 -0
- package/src/modules/billing/data/validators.ts +388 -8
- package/src/modules/billing/events.ts +56 -3
- package/src/modules/billing/i18n/de.json +880 -0
- package/src/modules/billing/i18n/en.json +457 -21
- package/src/modules/billing/i18n/es.json +880 -0
- package/src/modules/billing/i18n/pl.json +458 -22
- package/src/modules/billing/index.ts +6 -2
- package/src/modules/billing/lib/accountFinancials.ts +84 -0
- package/src/modules/billing/lib/accountLock.ts +32 -0
- package/src/modules/billing/lib/accountStatus.ts +11 -9
- package/src/modules/billing/lib/accountSummary.ts +172 -0
- package/src/modules/billing/lib/allocation.ts +515 -0
- package/src/modules/billing/lib/allocationEvents.ts +26 -0
- package/src/modules/billing/lib/billPeriod.ts +19 -0
- package/src/modules/billing/lib/billRunEngine.ts +350 -150
- package/src/modules/billing/lib/billingSettings.ts +145 -0
- package/src/modules/billing/lib/commandRoute.ts +112 -0
- package/src/modules/billing/lib/customerLookup.ts +157 -0
- package/src/modules/billing/lib/dateOnly.ts +22 -0
- package/src/modules/billing/lib/documentParties.ts +23 -0
- package/src/modules/billing/lib/documentTypes.ts +37 -0
- package/src/modules/billing/lib/dueDate.ts +26 -0
- package/src/modules/billing/lib/iban.ts +36 -0
- package/src/modules/billing/lib/invoiceDelivery.ts +196 -0
- package/src/modules/billing/lib/invoiceLanguages.ts +57 -0
- package/src/modules/billing/lib/invoiceWriter.ts +162 -148
- package/src/modules/billing/lib/itemBillingState.ts +67 -0
- package/src/modules/billing/lib/itemPriceChange.ts +56 -0
- package/src/modules/billing/lib/itemSelector.ts +4 -1
- package/src/modules/billing/lib/lineAmounts.ts +61 -0
- package/src/modules/billing/lib/memoGuards.ts +41 -0
- package/src/modules/billing/lib/navigation.ts +64 -0
- package/src/modules/billing/lib/numbering.ts +174 -0
- package/src/modules/billing/lib/overdue.ts +60 -0
- package/src/modules/billing/lib/paymentCsv.ts +171 -0
- package/src/modules/billing/lib/paymentGateway.ts +52 -0
- package/src/modules/billing/lib/paymentImport.ts +228 -0
- package/src/modules/billing/lib/paymentMatcher.ts +88 -0
- package/src/modules/billing/lib/pdf/attachmentStore.ts +60 -0
- package/src/modules/billing/lib/pdf/fonts/NotoSans-Bold.ttf +0 -0
- package/src/modules/billing/lib/pdf/fonts/NotoSans-Regular.ttf +0 -0
- package/src/modules/billing/lib/pdf/fonts/OFL.txt +93 -0
- package/src/modules/billing/lib/pdf/fonts/embeddedFonts.ts +9 -0
- package/src/modules/billing/lib/pdf/invoiceModel.ts +329 -0
- package/src/modules/billing/lib/pdf/invoicePdf.ts +220 -0
- package/src/modules/billing/lib/pdf/invoiceTemplate.ts +338 -0
- package/src/modules/billing/lib/prepaidConfig.ts +27 -36
- package/src/modules/billing/lib/prepaidConsume.ts +29 -6
- package/src/modules/billing/lib/prepaidPeriodClose.ts +407 -239
- package/src/modules/billing/lib/prepaidTaxPoint.ts +76 -0
- package/src/modules/billing/lib/seeds/demo.ts +79 -6
- package/src/modules/billing/lib/seeds/paymentsSample.ts +136 -0
- package/src/modules/billing/lib/seeds/sellerSettings.ts +78 -0
- package/src/modules/billing/lib/servicePeriods.ts +226 -0
- package/src/modules/billing/lib/testRunCleanup.ts +42 -0
- package/src/modules/billing/lib/topupCapture.ts +3 -4
- package/src/modules/billing/lib/topupReceipt.ts +70 -103
- package/src/modules/billing/lib/usageRater.ts +1 -1
- package/src/modules/billing/lib/usageRunner.ts +17 -5
- package/src/modules/billing/lib/vat.ts +56 -0
- package/src/modules/billing/migrations/Migration20261114000000_billing_customer_kind.ts +25 -0
- package/src/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.ts +37 -0
- package/src/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.ts +132 -0
- package/src/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.ts +58 -0
- package/src/modules/billing/migrations/Migration20261117000000_billing_payments.ts +97 -0
- package/src/modules/billing/migrations/Migration20261118000000_billing_memos.ts +21 -0
- package/src/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.ts +39 -0
- package/src/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.ts +35 -0
- package/src/modules/billing/setup.ts +11 -237
- package/src/modules/billing/subscribers/credit-memo-auto-send.ts +15 -0
- package/src/modules/billing/subscribers/debit-memo-apply-credit.ts +15 -0
- package/src/modules/billing/subscribers/debit-memo-auto-send.ts +15 -0
- package/src/modules/billing/subscribers/invoice-apply-credit.ts +39 -0
- package/src/modules/billing/subscribers/invoice-auto-send.ts +46 -0
- package/src/modules/billing/subscribers/payment-captured-to-paid.ts +84 -62
- package/src/modules/billing/subscribers/prepaid-topup-captured.ts +4 -8
- package/dist/modules/billing/lib/invoiceStatus.js +0 -38
- package/dist/modules/billing/lib/invoiceStatus.js.map +0 -7
- package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +0 -97
- package/src/modules/billing/lib/invoiceStatus.ts +0 -61
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { addDays, cloneAtMidnight, shiftCycles } from "./billPeriod.js";
|
|
2
|
+
const MAX_PERIODS_PER_ITEM = 120;
|
|
3
|
+
const DAY_MS = 864e5;
|
|
4
|
+
function toDate(value) {
|
|
5
|
+
return cloneAtMidnight(value);
|
|
6
|
+
}
|
|
7
|
+
function optDate(value) {
|
|
8
|
+
return value === null || value === void 0 || value === "" ? null : toDate(value);
|
|
9
|
+
}
|
|
10
|
+
function minDate(a, b) {
|
|
11
|
+
return a.getTime() <= b.getTime() ? a : b;
|
|
12
|
+
}
|
|
13
|
+
function maxDate(a, b) {
|
|
14
|
+
return a.getTime() >= b.getTime() ? a : b;
|
|
15
|
+
}
|
|
16
|
+
function inclusiveDays(from, to) {
|
|
17
|
+
return Math.round((to.getTime() - from.getTime()) / DAY_MS) + 1;
|
|
18
|
+
}
|
|
19
|
+
function roundHalfUp2dp(value) {
|
|
20
|
+
const sign = value < 0 ? -1 : 1;
|
|
21
|
+
return sign * Math.round((Math.abs(value) + Number.EPSILON) * 100) / 100;
|
|
22
|
+
}
|
|
23
|
+
function gridPeriod(anchor, cycle, index) {
|
|
24
|
+
const start = shiftCycles(anchor, cycle, index);
|
|
25
|
+
const end = addDays(shiftCycles(anchor, cycle, index + 1), -1);
|
|
26
|
+
return { index, start, end };
|
|
27
|
+
}
|
|
28
|
+
function gridPeriodContaining(anchor, cycle, date) {
|
|
29
|
+
const target = date.getTime();
|
|
30
|
+
const approxDays = { weekly: 7, monthly: 30.44, quarterly: 91.31, annually: 365.25 };
|
|
31
|
+
let index = Math.floor((target - anchor.getTime()) / DAY_MS / approxDays[cycle]);
|
|
32
|
+
let period = gridPeriod(anchor, cycle, index);
|
|
33
|
+
let guard = 0;
|
|
34
|
+
while (period.start.getTime() > target && guard++ < 1e3) period = gridPeriod(anchor, cycle, --index);
|
|
35
|
+
while (period.end.getTime() < target && guard++ < 2e3) period = gridPeriod(anchor, cycle, ++index);
|
|
36
|
+
return period;
|
|
37
|
+
}
|
|
38
|
+
function slice(item, anchor, cycle, from, to, kind) {
|
|
39
|
+
const lines = [];
|
|
40
|
+
const skipped = [];
|
|
41
|
+
let lastCovered = null;
|
|
42
|
+
let period = gridPeriodContaining(anchor, cycle, from);
|
|
43
|
+
let count = 0;
|
|
44
|
+
while (period.start.getTime() <= to.getTime()) {
|
|
45
|
+
if (count++ >= MAX_PERIODS_PER_ITEM) {
|
|
46
|
+
skipped.push({ billingItemId: item.id, reason: "period-cap-reached", servicePeriodStart: period.start, servicePeriodEnd: null });
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
const activeStart = maxDate(period.start, from);
|
|
50
|
+
const activeEnd = minDate(period.end, to);
|
|
51
|
+
const periodDays = inclusiveDays(period.start, period.end);
|
|
52
|
+
const activeDays = inclusiveDays(activeStart, activeEnd);
|
|
53
|
+
const partial = activeDays < periodDays;
|
|
54
|
+
if (partial && !item.prorate) {
|
|
55
|
+
skipped.push({
|
|
56
|
+
billingItemId: item.id,
|
|
57
|
+
reason: "partial-period-not-prorated",
|
|
58
|
+
servicePeriodStart: activeStart,
|
|
59
|
+
servicePeriodEnd: activeEnd
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
const gross = partial ? item.unitPrice * activeDays / periodDays : item.unitPrice;
|
|
63
|
+
const amount = roundHalfUp2dp(gross);
|
|
64
|
+
lines.push({
|
|
65
|
+
billingItemId: item.id,
|
|
66
|
+
kind,
|
|
67
|
+
servicePeriodStart: activeStart,
|
|
68
|
+
servicePeriodEnd: activeEnd,
|
|
69
|
+
amount: kind === "credit" ? -amount : amount,
|
|
70
|
+
proration: partial ? { activeDays, periodDays } : null
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
lastCovered = activeEnd;
|
|
74
|
+
period = gridPeriod(anchor, cycle, period.index + 1);
|
|
75
|
+
}
|
|
76
|
+
return { lines, skipped, lastCovered };
|
|
77
|
+
}
|
|
78
|
+
function computeRecurringLines(item, params) {
|
|
79
|
+
const billedTo = optDate(item.billedToDate);
|
|
80
|
+
if (item.deletedAt) {
|
|
81
|
+
return { lines: [], newBilledToDate: billedTo, skipped: [{ billingItemId: item.id, reason: "item-soft-deleted", servicePeriodStart: null, servicePeriodEnd: null }] };
|
|
82
|
+
}
|
|
83
|
+
if (!item.isActive) {
|
|
84
|
+
return { lines: [], newBilledToDate: billedTo, skipped: [{ billingItemId: item.id, reason: "item-inactive", servicePeriodStart: null, servicePeriodEnd: null }] };
|
|
85
|
+
}
|
|
86
|
+
const anchor = toDate(params.billDate);
|
|
87
|
+
const { cycle } = params;
|
|
88
|
+
const start = toDate(item.billStartDate);
|
|
89
|
+
const end = optDate(item.billEndDate);
|
|
90
|
+
if (end && billedTo && end.getTime() < billedTo.getTime()) {
|
|
91
|
+
if (!item.prorate) {
|
|
92
|
+
return {
|
|
93
|
+
lines: [],
|
|
94
|
+
newBilledToDate: billedTo,
|
|
95
|
+
skipped: [{ billingItemId: item.id, reason: "ended-after-billing-no-credit", servicePeriodStart: addDays(end, 1), servicePeriodEnd: billedTo }]
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const credit = slice(item, anchor, cycle, maxDate(addDays(end, 1), start), billedTo, "credit");
|
|
99
|
+
return { lines: credit.lines, newBilledToDate: end, skipped: credit.skipped };
|
|
100
|
+
}
|
|
101
|
+
const horizon = item.chargeTiming === "in_arrears" ? addDays(anchor, -1) : addDays(shiftCycles(anchor, cycle, 1), -1);
|
|
102
|
+
const cursor = billedTo ? maxDate(addDays(billedTo, 1), start) : start;
|
|
103
|
+
const windowEnd = end ? minDate(horizon, end) : horizon;
|
|
104
|
+
if (cursor.getTime() > windowEnd.getTime()) {
|
|
105
|
+
return { lines: [], newBilledToDate: billedTo, skipped: [] };
|
|
106
|
+
}
|
|
107
|
+
const charge = slice(item, anchor, cycle, cursor, windowEnd, "charge");
|
|
108
|
+
return {
|
|
109
|
+
lines: charge.lines,
|
|
110
|
+
newBilledToDate: charge.lastCovered ?? billedTo,
|
|
111
|
+
skipped: charge.skipped
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
MAX_PERIODS_PER_ITEM,
|
|
116
|
+
computeRecurringLines,
|
|
117
|
+
gridPeriodContaining,
|
|
118
|
+
inclusiveDays,
|
|
119
|
+
roundHalfUp2dp
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=servicePeriods.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/servicePeriods.ts"],
|
|
4
|
+
"sourcesContent": ["import type { BillCycle, ChargeTiming } from '../data/validators'\nimport { addDays, cloneAtMidnight, shiftCycles } from './billPeriod'\n\n/**\n * Recurring-charge computation for one bill date (Spec A).\n *\n * Pure and storage-agnostic: given an item's state and the account's bill\n * date, decide which service periods to charge (or credit) and where the\n * item's `billed_to_date` watermark moves. The engine calls it once per item\n * per simulated bill date and owns persistence.\n *\n * Model:\n * - The account's cycle grid is anchored at the bill date `D`:\n * \u2026, D \u2212 cycle, D, D + cycle, \u2026 (each period `[g_k, g_{k+1} \u2212 1]`).\n * - Horizon \u2014 how far the item is billed on `D`:\n * in_advance \u2192 D + cycle \u2212 1 in_arrears \u2192 D \u2212 1\n * - Cursor \u2014 first unbilled day: `billed_to_date + 1`, else `bill_start_date`.\n * - The unbilled window `[cursor, min(horizon, bill_end_date)]` is cut along\n * the grid; each grid period becomes one line (full \u2192 unit price,\n * partial \u2192 prorated by calendar days, or skipped when `prorate=false`).\n * - Credit (A-D2): an item ended (`bill_end_date`) before what was already\n * billed (`billed_to_date`) gets negative lines for the unused days when\n * `prorate=true`, and the watermark is pulled back to `bill_end_date`.\n *\n * The watermark is the single source of truth against double billing: every\n * computation starts at `billed_to_date + 1`, so switching timing, re-running\n * a bill date, or catching up can never charge a day twice.\n */\n\nexport type RecurringItemState = {\n id: string\n chargeTiming: ChargeTiming\n prorate: boolean\n billStartDate: Date | string\n billEndDate?: Date | string | null\n billedToDate?: Date | string | null\n unitPrice: number\n isActive: boolean\n deletedAt?: Date | string | null\n}\n\nexport type ProrationInfo = { activeDays: number; periodDays: number }\n\nexport type RecurringLine = {\n billingItemId: string\n kind: 'charge' | 'credit'\n /** Inclusive service period the line covers. */\n servicePeriodStart: Date\n servicePeriodEnd: Date\n /** 2dp half-up; negative for credits. */\n amount: number\n /** Null when the line covers a full grid period. */\n proration: ProrationInfo | null\n}\n\nexport type RecurringSkipReason =\n | 'item-inactive'\n | 'item-soft-deleted'\n | 'partial-period-not-prorated'\n | 'ended-after-billing-no-credit'\n | 'period-cap-reached'\n\nexport type RecurringSkip = {\n billingItemId: string\n reason: RecurringSkipReason\n servicePeriodStart: Date | null\n servicePeriodEnd: Date | null\n}\n\nexport type RecurringComputation = {\n lines: RecurringLine[]\n /** Watermark after these lines; unchanged when nothing was billed. */\n newBilledToDate: Date | null\n skipped: RecurringSkip[]\n}\n\n/** Upper bound of grid periods processed per item per call (runaway guard). */\nexport const MAX_PERIODS_PER_ITEM = 120\n\nconst DAY_MS = 86_400_000\n\nfunction toDate(value: Date | string | number): Date {\n return cloneAtMidnight(value)\n}\n\nfunction optDate(value: Date | string | null | undefined): Date | null {\n return value === null || value === undefined || value === '' ? null : toDate(value)\n}\n\nfunction minDate(a: Date, b: Date): Date {\n return a.getTime() <= b.getTime() ? a : b\n}\n\nfunction maxDate(a: Date, b: Date): Date {\n return a.getTime() >= b.getTime() ? a : b\n}\n\n/** Inclusive day count of `[from, to]`. */\nexport function inclusiveDays(from: Date, to: Date): number {\n return Math.round((to.getTime() - from.getTime()) / DAY_MS) + 1\n}\n\n/** Half-up 2dp rounding that survives binary floating point (1.005 \u2192 1.01). */\nexport function roundHalfUp2dp(value: number): number {\n const sign = value < 0 ? -1 : 1\n return (sign * Math.round((Math.abs(value) + Number.EPSILON) * 100)) / 100\n}\n\ntype GridPeriod = { index: number; start: Date; end: Date }\n\nfunction gridPeriod(anchor: Date, cycle: BillCycle, index: number): GridPeriod {\n const start = shiftCycles(anchor, cycle, index)\n const end = addDays(shiftCycles(anchor, cycle, index + 1), -1)\n return { index, start, end }\n}\n\n/** Grid period (anchored at `anchor`) containing `date`. */\nexport function gridPeriodContaining(anchor: Date, cycle: BillCycle, date: Date): GridPeriod {\n const target = date.getTime()\n // Coarse estimate first so long gaps don't walk one cycle at a time.\n const approxDays: Record<BillCycle, number> = { weekly: 7, monthly: 30.44, quarterly: 91.31, annually: 365.25 }\n let index = Math.floor((target - anchor.getTime()) / DAY_MS / approxDays[cycle])\n let period = gridPeriod(anchor, cycle, index)\n let guard = 0\n while (period.start.getTime() > target && guard++ < 1000) period = gridPeriod(anchor, cycle, --index)\n while (period.end.getTime() < target && guard++ < 2000) period = gridPeriod(anchor, cycle, ++index)\n return period\n}\n\n/**\n * Slices `[from, to]` along the grid and prices each slice. Shared by the\n * charge and credit paths so both prorate identically.\n */\nfunction slice(\n item: RecurringItemState,\n anchor: Date,\n cycle: BillCycle,\n from: Date,\n to: Date,\n kind: RecurringLine['kind'],\n): { lines: RecurringLine[]; skipped: RecurringSkip[]; lastCovered: Date | null } {\n const lines: RecurringLine[] = []\n const skipped: RecurringSkip[] = []\n let lastCovered: Date | null = null\n let period = gridPeriodContaining(anchor, cycle, from)\n let count = 0\n while (period.start.getTime() <= to.getTime()) {\n if (count++ >= MAX_PERIODS_PER_ITEM) {\n skipped.push({ billingItemId: item.id, reason: 'period-cap-reached', servicePeriodStart: period.start, servicePeriodEnd: null })\n break\n }\n const activeStart = maxDate(period.start, from)\n const activeEnd = minDate(period.end, to)\n const periodDays = inclusiveDays(period.start, period.end)\n const activeDays = inclusiveDays(activeStart, activeEnd)\n const partial = activeDays < periodDays\n if (partial && !item.prorate) {\n skipped.push({\n billingItemId: item.id,\n reason: 'partial-period-not-prorated',\n servicePeriodStart: activeStart,\n servicePeriodEnd: activeEnd,\n })\n } else {\n const gross = partial ? (item.unitPrice * activeDays) / periodDays : item.unitPrice\n const amount = roundHalfUp2dp(gross)\n lines.push({\n billingItemId: item.id,\n kind,\n servicePeriodStart: activeStart,\n servicePeriodEnd: activeEnd,\n amount: kind === 'credit' ? -amount : amount,\n proration: partial ? { activeDays, periodDays } : null,\n })\n }\n lastCovered = activeEnd\n period = gridPeriod(anchor, cycle, period.index + 1)\n }\n return { lines, skipped, lastCovered }\n}\n\nexport function computeRecurringLines(\n item: RecurringItemState,\n params: { billDate: Date | string; cycle: BillCycle },\n): RecurringComputation {\n const billedTo = optDate(item.billedToDate)\n if (item.deletedAt) {\n return { lines: [], newBilledToDate: billedTo, skipped: [{ billingItemId: item.id, reason: 'item-soft-deleted', servicePeriodStart: null, servicePeriodEnd: null }] }\n }\n if (!item.isActive) {\n return { lines: [], newBilledToDate: billedTo, skipped: [{ billingItemId: item.id, reason: 'item-inactive', servicePeriodStart: null, servicePeriodEnd: null }] }\n }\n\n const anchor = toDate(params.billDate)\n const { cycle } = params\n const start = toDate(item.billStartDate)\n const end = optDate(item.billEndDate)\n\n // \u2500\u2500 Credit: the item ended inside what was already billed (A-D2). \u2500\u2500\n if (end && billedTo && end.getTime() < billedTo.getTime()) {\n if (!item.prorate) {\n return {\n lines: [],\n newBilledToDate: billedTo,\n skipped: [{ billingItemId: item.id, reason: 'ended-after-billing-no-credit', servicePeriodStart: addDays(end, 1), servicePeriodEnd: billedTo }],\n }\n }\n const credit = slice(item, anchor, cycle, maxDate(addDays(end, 1), start), billedTo, 'credit')\n return { lines: credit.lines, newBilledToDate: end, skipped: credit.skipped }\n }\n\n // \u2500\u2500 Charge: bill the unbilled window up to the horizon. \u2500\u2500\n const horizon =\n item.chargeTiming === 'in_arrears' ? addDays(anchor, -1) : addDays(shiftCycles(anchor, cycle, 1), -1)\n const cursor = billedTo ? maxDate(addDays(billedTo, 1), start) : start\n const windowEnd = end ? minDate(horizon, end) : horizon\n if (cursor.getTime() > windowEnd.getTime()) {\n return { lines: [], newBilledToDate: billedTo, skipped: [] }\n }\n const charge = slice(item, anchor, cycle, cursor, windowEnd, 'charge')\n return {\n lines: charge.lines,\n newBilledToDate: charge.lastCovered ?? billedTo,\n skipped: charge.skipped,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
async function deleteTestRunArtifacts(em, scope) {
|
|
2
|
+
const params = [scope.tenantId, scope.organizationId];
|
|
3
|
+
let billRunClause = "";
|
|
4
|
+
if (scope.billRunId) {
|
|
5
|
+
billRunClause = " AND bill_run_id = ?";
|
|
6
|
+
params.push(scope.billRunId);
|
|
7
|
+
}
|
|
8
|
+
const rows = await em.execute(
|
|
9
|
+
`SELECT id FROM billing_invoices
|
|
10
|
+
WHERE tenant_id = ? AND organization_id = ? AND test_run = true AND document_type = 'invoice'${billRunClause}`,
|
|
11
|
+
params
|
|
12
|
+
);
|
|
13
|
+
const ids = (rows ?? []).map((row) => row.id);
|
|
14
|
+
if (ids.length === 0) return 0;
|
|
15
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
16
|
+
await em.execute(`DELETE FROM billing_draft_invoice_edits WHERE invoice_id IN (${placeholders})`, ids);
|
|
17
|
+
await em.execute(`DELETE FROM billing_invoice_lines WHERE invoice_id IN (${placeholders})`, ids);
|
|
18
|
+
await em.execute(`DELETE FROM billing_invoices WHERE id IN (${placeholders})`, ids);
|
|
19
|
+
return ids.length;
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
deleteTestRunArtifacts
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=testRunCleanup.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/testRunCleanup.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\n\n/**\n * Test-run cleanup shared by the \"wipe test invoices\" and \"delete run\"\n * commands (Spec F, P-D1).\n *\n * Hard-deletes test drafts (`test_run = true`, `document_type = 'invoice'`)\n * together with their lines and their draft-edit audit rows. Scoped by\n * tenant + organization, and optionally by one Bill Run. Must run inside the\n * caller's transaction.\n */\n\nexport type TestRunCleanupScope = {\n tenantId: string\n organizationId: string\n /** When set, only test invoices written by this Bill Run are removed. */\n billRunId?: string | null\n}\n\nexport async function deleteTestRunArtifacts(\n em: EntityManager,\n scope: TestRunCleanupScope,\n): Promise<number> {\n const params: unknown[] = [scope.tenantId, scope.organizationId]\n let billRunClause = ''\n if (scope.billRunId) {\n billRunClause = ' AND bill_run_id = ?'\n params.push(scope.billRunId)\n }\n const rows = (await em.execute(\n `SELECT id FROM billing_invoices\n WHERE tenant_id = ? AND organization_id = ? AND test_run = true AND document_type = 'invoice'${billRunClause}`,\n params,\n )) as unknown as Array<{ id: string }> | undefined\n const ids = (rows ?? []).map((row) => row.id)\n if (ids.length === 0) return 0\n const placeholders = ids.map(() => '?').join(',')\n await em.execute(`DELETE FROM billing_draft_invoice_edits WHERE invoice_id IN (${placeholders})`, ids)\n await em.execute(`DELETE FROM billing_invoice_lines WHERE invoice_id IN (${placeholders})`, ids)\n await em.execute(`DELETE FROM billing_invoices WHERE id IN (${placeholders})`, ids)\n return ids.length\n}\n"],
|
|
5
|
+
"mappings": "AAmBA,eAAsB,uBACpB,IACA,OACiB;AACjB,QAAM,SAAoB,CAAC,MAAM,UAAU,MAAM,cAAc;AAC/D,MAAI,gBAAgB;AACpB,MAAI,MAAM,WAAW;AACnB,oBAAgB;AAChB,WAAO,KAAK,MAAM,SAAS;AAAA,EAC7B;AACA,QAAM,OAAQ,MAAM,GAAG;AAAA,IACrB;AAAA,oGACgG,aAAa;AAAA,IAC7G;AAAA,EACF;AACA,QAAM,OAAO,QAAQ,CAAC,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE;AAC5C,MAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,QAAM,eAAe,IAAI,IAAI,MAAM,GAAG,EAAE,KAAK,GAAG;AAChD,QAAM,GAAG,QAAQ,gEAAgE,YAAY,KAAK,GAAG;AACrG,QAAM,GAAG,QAAQ,0DAA0D,YAAY,KAAK,GAAG;AAC/F,QAAM,GAAG,QAAQ,6CAA6C,YAAY,KAAK,GAAG;AAClF,SAAO,IAAI;AACb;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BillingTopup } from "../data/entities.js";
|
|
2
2
|
import { applyBalanceMovement } from "./balanceLedger.js";
|
|
3
3
|
import { createTopupReceipt } from "./topupReceipt.js";
|
|
4
|
-
async function captureTopup(em,
|
|
4
|
+
async function captureTopup(em, params) {
|
|
5
5
|
const { topup, account } = params;
|
|
6
6
|
if (topup.status === "captured") {
|
|
7
7
|
return { status: "already_captured" };
|
|
@@ -32,7 +32,7 @@ async function captureTopup(em, deps, params) {
|
|
|
32
32
|
receiptInvoiceId: prior?.receiptInvoiceId ?? void 0
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
const receipt = await createTopupReceipt(tem,
|
|
35
|
+
const receipt = await createTopupReceipt(tem, {
|
|
36
36
|
account,
|
|
37
37
|
topupId: topup.id,
|
|
38
38
|
paymentId: topup.paymentId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/lib/topupCapture.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { BillingTopup } from '../data/entities'\nimport { applyBalanceMovement } from './balanceLedger'\nimport { createTopupReceipt
|
|
5
|
-
"mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,4BAA4B;AACrC,SAAS,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { BillingTopup } from '../data/entities'\nimport { applyBalanceMovement } from './balanceLedger'\nimport { createTopupReceipt } from './topupReceipt'\n\n/**\n * Idempotent top-up capture (SPEC-002 P2).\n *\n * On `payment_gateways.payment.captured` for a top-up, in ONE DB transaction:\n * - credit the balance (atomic) + append a `type='topup'` transaction with a\n * deterministic `source_ref = topup-{paymentId}` (so a re-fired capture can\n * never double-credit),\n * - create the paid VAT receipt as a billing document (`document_type = 'receipt'`),\n * - transition the BillingTopup \u2192 captured, backfilling `transaction_id` +\n * `receipt_invoice_id`.\n *\n * Re-firing on an already-captured top-up is a no-op (the gateway delivers\n * webhooks at-least-once).\n */\n\nexport type CaptureTopupAccount = {\n id: string\n organizationId: string\n tenantId: string\n currencyCode: string\n}\n\nexport type CaptureTopupParams = {\n topup: BillingTopup\n account: CaptureTopupAccount\n /** VAT percent applied to the receipt. */\n taxRate: number\n /** Provider transaction id for the transaction metadata, if known. */\n gatewayTransactionId?: string | null\n}\n\nexport type CaptureTopupResult = {\n status: 'captured' | 'already_captured'\n balanceAfter?: string\n transactionId?: string\n receiptInvoiceId?: string\n invoiceNumber?: string\n grossAmount?: string\n}\n\nexport async function captureTopup(\n em: EntityManager,\n params: CaptureTopupParams,\n): Promise<CaptureTopupResult> {\n const { topup, account } = params\n\n if (topup.status === 'captured') {\n return { status: 'already_captured' }\n }\n\n return em.transactional(async (tem) => {\n // 1. Atomic credit + paired transaction (idempotent on source_ref).\n const movement = await applyBalanceMovement(tem as EntityManager, {\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n billAccountId: account.id,\n currencyCode: account.currencyCode,\n type: 'topup',\n amount: topup.amount,\n description: `Top-up \u2014 ${topup.providerKey}`,\n topupId: topup.id,\n sourceRef: `topup-${topup.paymentId}`,\n metadata: {\n provider_key: topup.providerKey,\n payment_id: topup.paymentId,\n gateway_transaction_id: params.gatewayTransactionId ?? topup.gatewayTransactionId ?? null,\n },\n })\n\n // A deduplicated credit means a PRIOR capture already committed (the\n // gateway delivers `payment.captured` at-least-once, and the stale\n // `status` check above can race a concurrent capture). Do NOT issue a\n // second fiscal receipt \u2014 return the existing capture's links.\n if (movement.deduplicated) {\n const prior = await tem.findOne(BillingTopup, { id: topup.id })\n return {\n status: 'already_captured' as const,\n balanceAfter: movement.balanceAfter,\n transactionId: prior?.transactionId ?? movement.transaction.id,\n receiptInvoiceId: prior?.receiptInvoiceId ?? undefined,\n }\n }\n\n // 2. Fiscal VAT receipt (posted+paid).\n const receipt = await createTopupReceipt(tem as EntityManager, {\n account,\n topupId: topup.id,\n paymentId: topup.paymentId,\n amount: topup.amount,\n taxRate: params.taxRate,\n })\n\n // 3. Transition the top-up.\n const managed = await tem.findOne(BillingTopup, { id: topup.id })\n const target = managed ?? topup\n target.status = 'captured'\n target.transactionId = movement.transaction.id\n target.receiptInvoiceId = receipt.invoiceId\n target.updatedAt = new Date()\n await tem.flush()\n\n return {\n status: 'captured' as const,\n balanceAfter: movement.balanceAfter,\n transactionId: movement.transaction.id,\n receiptInvoiceId: receipt.invoiceId,\n invoiceNumber: receipt.invoiceNumber,\n grossAmount: receipt.grossAmount,\n }\n })\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,4BAA4B;AACrC,SAAS,0BAA0B;AA0CnC,eAAsB,aACpB,IACA,QAC6B;AAC7B,QAAM,EAAE,OAAO,QAAQ,IAAI;AAE3B,MAAI,MAAM,WAAW,YAAY;AAC/B,WAAO,EAAE,QAAQ,mBAAmB;AAAA,EACtC;AAEA,SAAO,GAAG,cAAc,OAAO,QAAQ;AAErC,UAAM,WAAW,MAAM,qBAAqB,KAAsB;AAAA,MAChE,gBAAgB,QAAQ;AAAA,MACxB,UAAU,QAAQ;AAAA,MAClB,eAAe,QAAQ;AAAA,MACvB,cAAc,QAAQ;AAAA,MACtB,MAAM;AAAA,MACN,QAAQ,MAAM;AAAA,MACd,aAAa,iBAAY,MAAM,WAAW;AAAA,MAC1C,SAAS,MAAM;AAAA,MACf,WAAW,SAAS,MAAM,SAAS;AAAA,MACnC,UAAU;AAAA,QACR,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM;AAAA,QAClB,wBAAwB,OAAO,wBAAwB,MAAM,wBAAwB;AAAA,MACvF;AAAA,IACF,CAAC;AAMD,QAAI,SAAS,cAAc;AACzB,YAAM,QAAQ,MAAM,IAAI,QAAQ,cAAc,EAAE,IAAI,MAAM,GAAG,CAAC;AAC9D,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,cAAc,SAAS;AAAA,QACvB,eAAe,OAAO,iBAAiB,SAAS,YAAY;AAAA,QAC5D,kBAAkB,OAAO,oBAAoB;AAAA,MAC/C;AAAA,IACF;AAGA,UAAM,UAAU,MAAM,mBAAmB,KAAsB;AAAA,MAC7D;AAAA,MACA,SAAS,MAAM;AAAA,MACf,WAAW,MAAM;AAAA,MACjB,QAAQ,MAAM;AAAA,MACd,SAAS,OAAO;AAAA,IAClB,CAAC;AAGD,UAAM,UAAU,MAAM,IAAI,QAAQ,cAAc,EAAE,IAAI,MAAM,GAAG,CAAC;AAChE,UAAM,SAAS,WAAW;AAC1B,WAAO,SAAS;AAChB,WAAO,gBAAgB,SAAS,YAAY;AAC5C,WAAO,mBAAmB,QAAQ;AAClC,WAAO,YAAY,oBAAI,KAAK;AAC5B,UAAM,IAAI,MAAM;AAEhB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,cAAc,SAAS;AAAA,MACvB,eAAe,SAAS,YAAY;AAAA,MACpC,kBAAkB,QAAQ;AAAA,MAC1B,eAAe,QAAQ;AAAA,MACvB,aAAa,QAAQ;AAAA,IACvB;AAAA,EACF,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,94 +1,73 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return (
|
|
1
|
+
import { BillingInvoice, BillingInvoiceLine } from "../data/entities.js";
|
|
2
|
+
import { getBillingSettings, numberMaskFor } from "./billingSettings.js";
|
|
3
|
+
import { claimDocumentNumber } from "./numbering.js";
|
|
4
|
+
import { lineAmountsFromGross, money4 } from "./vat.js";
|
|
5
|
+
function todayUtc() {
|
|
6
|
+
const now = /* @__PURE__ */ new Date();
|
|
7
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
8
8
|
}
|
|
9
|
-
async function createTopupReceipt(em,
|
|
9
|
+
async function createTopupReceipt(em, params) {
|
|
10
10
|
const { account } = params;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const taxAmount = fmt4(tax.taxAmount);
|
|
21
|
-
const effectiveRate = tax.taxRate ?? 0;
|
|
22
|
-
const generated = await deps.numberGenerator.generate({
|
|
23
|
-
kind: "invoice",
|
|
24
|
-
organizationId: account.organizationId,
|
|
25
|
-
tenantId: account.tenantId
|
|
26
|
-
});
|
|
27
|
-
const paidStatusEntryId = await resolveInvoiceStatusEntryId(
|
|
28
|
-
em,
|
|
29
|
-
account.tenantId,
|
|
30
|
-
account.organizationId,
|
|
31
|
-
"paid"
|
|
32
|
-
);
|
|
11
|
+
const scope = { tenantId: account.tenantId, organizationId: account.organizationId };
|
|
12
|
+
const taxRate = Number.isFinite(params.taxRate) && params.taxRate > 0 ? params.taxRate : 0;
|
|
13
|
+
const amounts = lineAmountsFromGross(Number.parseFloat(params.amount), taxRate);
|
|
14
|
+
const net = money4(amounts.net);
|
|
15
|
+
const tax = money4(amounts.tax);
|
|
16
|
+
const gross = money4(amounts.gross);
|
|
17
|
+
const settings = await getBillingSettings(em, scope);
|
|
18
|
+
const issueDate = todayUtc();
|
|
19
|
+
const claimed = await claimDocumentNumber(em, scope, "receipt", numberMaskFor(settings, "receipt"), issueDate);
|
|
33
20
|
const now = /* @__PURE__ */ new Date();
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
const receipt = em.create(BillingInvoice, {
|
|
22
|
+
...scope,
|
|
23
|
+
billAccountId: account.id,
|
|
24
|
+
documentType: "receipt",
|
|
25
|
+
number: claimed.number,
|
|
26
|
+
series: "receipt",
|
|
39
27
|
status: "paid",
|
|
40
|
-
|
|
28
|
+
testRun: false,
|
|
29
|
+
issueDate,
|
|
30
|
+
postedAt: now,
|
|
41
31
|
currencyCode: account.currencyCode,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
metadata: {
|
|
51
|
-
kind: "prepaid_topup",
|
|
52
|
-
bill_account_id: account.id,
|
|
53
|
-
topup_id: params.topupId,
|
|
54
|
-
payment_id: params.paymentId
|
|
55
|
-
},
|
|
32
|
+
subtotalNet: net,
|
|
33
|
+
taxTotal: tax,
|
|
34
|
+
totalGross: gross,
|
|
35
|
+
paidTotal: gross,
|
|
36
|
+
creditedTotal: "0",
|
|
37
|
+
outstanding: "0",
|
|
38
|
+
topupId: params.topupId,
|
|
39
|
+
metadata: { kind: "prepaid_topup", payment_id: params.paymentId },
|
|
56
40
|
createdAt: now,
|
|
57
41
|
updatedAt: now
|
|
58
42
|
});
|
|
59
|
-
em.persist(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
kind: "prepaid_topup",
|
|
80
|
-
topup_id: params.topupId
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
em.persist(line);
|
|
43
|
+
em.persist(receipt);
|
|
44
|
+
await em.flush();
|
|
45
|
+
em.persist(
|
|
46
|
+
em.create(BillingInvoiceLine, {
|
|
47
|
+
...scope,
|
|
48
|
+
invoiceId: receipt.id,
|
|
49
|
+
lineNumber: 1,
|
|
50
|
+
billingType: "receipt",
|
|
51
|
+
description: "Account top-up",
|
|
52
|
+
quantity: "1.0000",
|
|
53
|
+
unitPriceNet: net,
|
|
54
|
+
taxRate: taxRate.toFixed(4),
|
|
55
|
+
netAmount: net,
|
|
56
|
+
taxAmount: tax,
|
|
57
|
+
grossAmount: gross,
|
|
58
|
+
metadata: { kind: "prepaid_topup", topup_id: params.topupId },
|
|
59
|
+
createdAt: now,
|
|
60
|
+
updatedAt: now
|
|
61
|
+
})
|
|
62
|
+
);
|
|
84
63
|
await em.flush();
|
|
85
64
|
return {
|
|
86
|
-
invoiceId:
|
|
87
|
-
invoiceNumber:
|
|
65
|
+
invoiceId: receipt.id,
|
|
66
|
+
invoiceNumber: claimed.number,
|
|
88
67
|
netAmount: net,
|
|
89
|
-
taxAmount,
|
|
68
|
+
taxAmount: tax,
|
|
90
69
|
grossAmount: gross,
|
|
91
|
-
taxRate
|
|
70
|
+
taxRate
|
|
92
71
|
};
|
|
93
72
|
}
|
|
94
73
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/lib/topupReceipt.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport {
|
|
5
|
-
"mappings": "AACA
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { BillingInvoice, BillingInvoiceLine } from '../data/entities'\nimport { getBillingSettings, numberMaskFor } from './billingSettings'\nimport { claimDocumentNumber } from './numbering'\nimport { lineAmountsFromGross, money4 } from './vat'\n\n/**\n * Creates the fiscal VAT receipt for a captured prepaid top-up (SPEC-002 P2,\n * Spec 0 E-D3).\n *\n * The receipt is a billing-owned document (`billing_invoices` with\n * `document_type = 'receipt'`, `status = 'paid'`), numbered from the\n * `receipt` series (default mask `RC/{YYYY}/{MM}/{SEQUENCE_NUM_MONTH:4}`).\n * Nothing is written to core `sales`.\n *\n * VAT at top-up: the top-up `amount` is the GROSS the customer paid (= the\n * balance credited); `vat.lineAmountsFromGross` splits it into net + tax with\n * `net + tax = gross` exactly.\n *\n * MUST run inside the capture transaction (`captureTopup`) so the receipt\n * number, the balance credit and the top-up transition commit together.\n */\n\nexport type TopupReceiptParams = {\n account: {\n id: string\n organizationId: string\n tenantId: string\n currencyCode: string\n }\n topupId: string\n paymentId: string\n /** Gross amount paid (numeric(18,4) string). */\n amount: string\n /** VAT percent (e.g. 23). 0 = no VAT. */\n taxRate: number\n}\n\nexport type TopupReceipt = {\n invoiceId: string\n invoiceNumber: string\n netAmount: string\n taxAmount: string\n grossAmount: string\n taxRate: number\n}\n\nfunction todayUtc(): Date {\n const now = new Date()\n return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))\n}\n\nexport async function createTopupReceipt(em: EntityManager, params: TopupReceiptParams): Promise<TopupReceipt> {\n const { account } = params\n const scope = { tenantId: account.tenantId, organizationId: account.organizationId }\n const taxRate = Number.isFinite(params.taxRate) && params.taxRate > 0 ? params.taxRate : 0\n const amounts = lineAmountsFromGross(Number.parseFloat(params.amount), taxRate)\n const net = money4(amounts.net)\n const tax = money4(amounts.tax)\n const gross = money4(amounts.gross)\n\n const settings = await getBillingSettings(em, scope)\n const issueDate = todayUtc()\n const claimed = await claimDocumentNumber(em, scope, 'receipt', numberMaskFor(settings, 'receipt'), issueDate)\n\n const now = new Date()\n const receipt = em.create(BillingInvoice, {\n ...scope,\n billAccountId: account.id,\n documentType: 'receipt',\n number: claimed.number,\n series: 'receipt',\n status: 'paid',\n testRun: false,\n issueDate,\n postedAt: now,\n currencyCode: account.currencyCode,\n subtotalNet: net,\n taxTotal: tax,\n totalGross: gross,\n paidTotal: gross,\n creditedTotal: '0',\n outstanding: '0',\n topupId: params.topupId,\n metadata: { kind: 'prepaid_topup', payment_id: params.paymentId },\n createdAt: now,\n updatedAt: now,\n })\n em.persist(receipt)\n await em.flush() // need the receipt id for the line\n\n em.persist(\n em.create(BillingInvoiceLine, {\n ...scope,\n invoiceId: receipt.id,\n lineNumber: 1,\n billingType: 'receipt',\n description: 'Account top-up',\n quantity: '1.0000',\n unitPriceNet: net,\n taxRate: taxRate.toFixed(4),\n netAmount: net,\n taxAmount: tax,\n grossAmount: gross,\n metadata: { kind: 'prepaid_topup', topup_id: params.topupId },\n createdAt: now,\n updatedAt: now,\n }),\n )\n await em.flush()\n\n return {\n invoiceId: receipt.id,\n invoiceNumber: claimed.number,\n netAmount: net,\n taxAmount: tax,\n grossAmount: gross,\n taxRate,\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,gBAAgB,0BAA0B;AACnD,SAAS,oBAAoB,qBAAqB;AAClD,SAAS,2BAA2B;AACpC,SAAS,sBAAsB,cAAc;AA2C7C,SAAS,WAAiB;AACxB,QAAM,MAAM,oBAAI,KAAK;AACrB,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,eAAe,GAAG,IAAI,YAAY,GAAG,IAAI,WAAW,CAAC,CAAC;AACrF;AAEA,eAAsB,mBAAmB,IAAmB,QAAmD;AAC7G,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,QAAQ,EAAE,UAAU,QAAQ,UAAU,gBAAgB,QAAQ,eAAe;AACnF,QAAM,UAAU,OAAO,SAAS,OAAO,OAAO,KAAK,OAAO,UAAU,IAAI,OAAO,UAAU;AACzF,QAAM,UAAU,qBAAqB,OAAO,WAAW,OAAO,MAAM,GAAG,OAAO;AAC9E,QAAM,MAAM,OAAO,QAAQ,GAAG;AAC9B,QAAM,MAAM,OAAO,QAAQ,GAAG;AAC9B,QAAM,QAAQ,OAAO,QAAQ,KAAK;AAElC,QAAM,WAAW,MAAM,mBAAmB,IAAI,KAAK;AACnD,QAAM,YAAY,SAAS;AAC3B,QAAM,UAAU,MAAM,oBAAoB,IAAI,OAAO,WAAW,cAAc,UAAU,SAAS,GAAG,SAAS;AAE7G,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,UAAU,GAAG,OAAO,gBAAgB;AAAA,IACxC,GAAG;AAAA,IACH,eAAe,QAAQ;AAAA,IACvB,cAAc;AAAA,IACd,QAAQ,QAAQ;AAAA,IAChB,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,IACT;AAAA,IACA,UAAU;AAAA,IACV,cAAc,QAAQ;AAAA,IACtB,aAAa;AAAA,IACb,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,eAAe;AAAA,IACf,aAAa;AAAA,IACb,SAAS,OAAO;AAAA,IAChB,UAAU,EAAE,MAAM,iBAAiB,YAAY,OAAO,UAAU;AAAA,IAChE,WAAW;AAAA,IACX,WAAW;AAAA,EACb,CAAC;AACD,KAAG,QAAQ,OAAO;AAClB,QAAM,GAAG,MAAM;AAEf,KAAG;AAAA,IACD,GAAG,OAAO,oBAAoB;AAAA,MAC5B,GAAG;AAAA,MACH,WAAW,QAAQ;AAAA,MACnB,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,aAAa;AAAA,MACb,UAAU;AAAA,MACV,cAAc;AAAA,MACd,SAAS,QAAQ,QAAQ,CAAC;AAAA,MAC1B,WAAW;AAAA,MACX,WAAW;AAAA,MACX,aAAa;AAAA,MACb,UAAU,EAAE,MAAM,iBAAiB,UAAU,OAAO,QAAQ;AAAA,MAC5D,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AAAA,EACH;AACA,QAAM,GAAG,MAAM;AAEf,SAAO;AAAA,IACL,WAAW,QAAQ;AAAA,IACnB,eAAe,QAAQ;AAAA,IACvB,WAAW;AAAA,IACX,WAAW;AAAA,IACX,aAAa;AAAA,IACb;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/lib/usageRater.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Pure functions to rate usage quantity against a `rate_json` payload.\n *\n * Three tier models per the spec (`volume` / `graduated` / `flat`) plus\n * the \"simple\" flat-rate shape (`{ unit_price }` without a `model`).\n * The validator at `data/validators.ts` ensures the payload structure\n * before it reaches this rater \u2014 this module trusts the shape but\n * defends against numeric edge cases (NaN, negative quantity).\n *\n * Output:\n * - `amount` \u2014 numeric string, rounded 2dp half-up, formatted to 4\n * decimals to match the `core/sales` numeric(18,4) column shape.\n * - `breakdown` \u2014 per-tier rated quantity / amount, persisted into\n * `
|
|
4
|
+
"sourcesContent": ["/**\n * Pure functions to rate usage quantity against a `rate_json` payload.\n *\n * Three tier models per the spec (`volume` / `graduated` / `flat`) plus\n * the \"simple\" flat-rate shape (`{ unit_price }` without a `model`).\n * The validator at `data/validators.ts` ensures the payload structure\n * before it reaches this rater \u2014 this module trusts the shape but\n * defends against numeric edge cases (NaN, negative quantity).\n *\n * Output:\n * - `amount` \u2014 numeric string, rounded 2dp half-up, formatted to 4\n * decimals to match the `core/sales` numeric(18,4) column shape.\n * - `breakdown` \u2014 per-tier rated quantity / amount, persisted into\n * `billing_invoice_lines.metadata.usage_tier_breakdown` so operators\n * can see how the line was built without re-running the math.\n */\n\nimport type { BillingItem } from '../data/entities'\n\nexport type SimpleFlatRate = { unit_price: number }\nexport type VolumeTier = { up_to: number | null; unit_price: number }\nexport type GraduatedTier = { up_to: number | null; unit_price: number }\nexport type FlatTier = { up_to: number | null; flat_amount: number }\n\nexport type VolumeRate = { model: 'volume'; tiers: VolumeTier[] }\nexport type GraduatedRate = { model: 'graduated'; tiers: GraduatedTier[] }\nexport type FlatRate = { model: 'flat'; tiers: FlatTier[] }\nexport type TieredRate = VolumeRate | GraduatedRate | FlatRate\nexport type UsageRate = SimpleFlatRate | TieredRate\n\nexport type TierBreakdownEntry = {\n tierIndex: number\n upTo: number | null\n /** Quantity rated at this tier \u2014 0 for tiers above the consumed range. */\n rangeQuantity: number\n /** `unit_price` for volume/graduated; `flat_amount` for flat. */\n rate: number\n /** Per-tier amount contribution, rounded 4dp. */\n amount: string\n}\n\nexport type UsageRateResult = {\n /** Total rated amount, numeric(18,4) string. */\n amount: string\n /** Per-tier or single-rate breakdown for invoice-line metadata. */\n breakdown: TierBreakdownEntry[]\n}\n\n/**\n * Half-up rounding to 2dp survives the JS floating-point edge case\n * (`1.005 \u2192 1.01`, `100 \u00D7 0.001 + 100 \u00D7 0.001 + \u2026 \u2192 exactly N\u00D70.001`).\n * Match `invoiceWriter`'s rounding policy so totals are consistent\n * across line types.\n */\nfunction roundHalfUp2dp(value: number): number {\n return Math.round((value + Number.EPSILON) * 100) / 100\n}\n\nfunction format4dp(value: number): string {\n return roundHalfUp2dp(value).toFixed(4)\n}\n\n/**\n * Format a unit price to 4dp WITHOUT 2dp rounding \u2014 sub-cent rates\n * like `0.001` (per-API-request pricing) are legitimate. The spec's\n * 2dp half-up rule applies only to line *amounts*, not per-unit\n * rates.\n */\nfunction formatUnitPrice(value: number): string {\n if (!Number.isFinite(value)) return '0.0000'\n return value.toFixed(4)\n}\n\nfunction isTieredRate(rate: UsageRate): rate is TieredRate {\n return typeof (rate as { model?: unknown }).model === 'string'\n}\n\nfunction safeQuantity(quantity: number): number {\n if (!Number.isFinite(quantity) || quantity < 0) return 0\n return quantity\n}\n\n// \u2500\u2500\u2500 Simple flat-rate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction rateSimpleFlat(quantity: number, rate: SimpleFlatRate): UsageRateResult {\n const q = safeQuantity(quantity)\n const total = q * rate.unit_price\n return {\n amount: format4dp(total),\n breakdown: [\n {\n tierIndex: 0,\n upTo: null,\n rangeQuantity: q,\n rate: rate.unit_price,\n amount: format4dp(total),\n },\n ],\n }\n}\n\n// \u2500\u2500\u2500 Volume tier \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// \"Entire quantity priced at the unit_price of the tier its total falls\n// into.\" So we walk tiers in order, find the first tier whose `up_to`\n// is `null` (open-ended) or `>= quantity`, and charge `quantity \u00D7\n// that tier's unit_price`. The unconsumed tiers report 0.\n\nfunction rateVolume(quantity: number, rate: VolumeRate): UsageRateResult {\n const q = safeQuantity(quantity)\n const breakdown: TierBreakdownEntry[] = rate.tiers.map((tier, idx) => ({\n tierIndex: idx,\n upTo: tier.up_to,\n rangeQuantity: 0,\n rate: tier.unit_price,\n amount: format4dp(0),\n }))\n for (let i = 0; i < rate.tiers.length; i++) {\n const tier = rate.tiers[i]\n if (tier.up_to === null || q <= tier.up_to) {\n const total = q * tier.unit_price\n breakdown[i] = {\n tierIndex: i,\n upTo: tier.up_to,\n rangeQuantity: q,\n rate: tier.unit_price,\n amount: format4dp(total),\n }\n return { amount: format4dp(total), breakdown }\n }\n }\n // Should be unreachable: validator enforces an open-ended top tier.\n return { amount: format4dp(0), breakdown }\n}\n\n// \u2500\u2500\u2500 Graduated tier \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// \"Each unit priced at its tier.\" Walk tiers in order, charging\n// `min(q_remaining, tier_capacity) \u00D7 unit_price` for each tier the\n// quantity touches.\n\nfunction rateGraduated(quantity: number, rate: GraduatedRate): UsageRateResult {\n const q = safeQuantity(quantity)\n let remaining = q\n let previousUpTo = 0\n let total = 0\n const breakdown: TierBreakdownEntry[] = []\n for (let i = 0; i < rate.tiers.length; i++) {\n const tier = rate.tiers[i]\n const tierCapacity =\n tier.up_to === null ? Infinity : Math.max(0, tier.up_to - previousUpTo)\n const consumed = Math.min(remaining, tierCapacity)\n const tierAmount = consumed * tier.unit_price\n total += tierAmount\n breakdown.push({\n tierIndex: i,\n upTo: tier.up_to,\n rangeQuantity: consumed,\n rate: tier.unit_price,\n amount: format4dp(tierAmount),\n })\n remaining -= consumed\n previousUpTo = tier.up_to ?? previousUpTo\n if (remaining <= 0) {\n // Pad remaining tiers with zeros for breakdown completeness.\n for (let j = i + 1; j < rate.tiers.length; j++) {\n breakdown.push({\n tierIndex: j,\n upTo: rate.tiers[j].up_to,\n rangeQuantity: 0,\n rate: rate.tiers[j].unit_price,\n amount: format4dp(0),\n })\n }\n break\n }\n }\n return { amount: format4dp(total), breakdown }\n}\n\n// \u2500\u2500\u2500 Flat tier \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// \"One flat fee for the highest tier reached.\" Find the first tier\n// whose `up_to` is `null` or `>= quantity`. Charge that tier's\n// `flat_amount`. Other tiers report 0.\n\nfunction rateFlat(quantity: number, rate: FlatRate): UsageRateResult {\n const q = safeQuantity(quantity)\n const breakdown: TierBreakdownEntry[] = rate.tiers.map((tier, idx) => ({\n tierIndex: idx,\n upTo: tier.up_to,\n rangeQuantity: 0,\n rate: tier.flat_amount,\n amount: format4dp(0),\n }))\n for (let i = 0; i < rate.tiers.length; i++) {\n const tier = rate.tiers[i]\n if (tier.up_to === null || q <= tier.up_to) {\n breakdown[i] = {\n tierIndex: i,\n upTo: tier.up_to,\n rangeQuantity: q,\n rate: tier.flat_amount,\n amount: format4dp(tier.flat_amount),\n }\n return { amount: format4dp(tier.flat_amount), breakdown }\n }\n }\n return { amount: format4dp(0), breakdown }\n}\n\n// \u2500\u2500\u2500 Public dispatcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function rateUsageQuantity(rate: UsageRate, totalQuantity: number): UsageRateResult {\n if (!isTieredRate(rate)) {\n return rateSimpleFlat(totalQuantity, rate)\n }\n switch (rate.model) {\n case 'volume':\n return rateVolume(totalQuantity, rate)\n case 'graduated':\n return rateGraduated(totalQuantity, rate)\n case 'flat':\n return rateFlat(totalQuantity, rate)\n }\n}\n\n/**\n * Convenience wrapper: takes a BillingItem's `rate_json` (typed as\n * `unknown` from JSONB) and the total quantity, returns the rated\n * amount + breakdown. Returns `null` when the rate shape is invalid \u2014\n * caller falls back to \"no charge\" + warns instead of crashing the\n * whole account.\n */\nexport function rateUsageForItem(\n item: Pick<BillingItem, 'rateJson' | 'type'>,\n totalQuantity: number,\n): UsageRateResult | null {\n if (item.type !== 'usage') return null\n const rate = item.rateJson as UsageRate | null | undefined\n if (!rate || typeof rate !== 'object') return null\n if (isTieredRate(rate)) {\n if (!Array.isArray(rate.tiers) || rate.tiers.length === 0) return null\n } else if (typeof (rate as SimpleFlatRate).unit_price !== 'number') {\n return null\n }\n return rateUsageQuantity(rate, totalQuantity)\n}\n\nexport { roundHalfUp2dp, format4dp, formatUnitPrice }\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { itemTaxRate } from "./invoiceWriter.js";
|
|
1
2
|
import { format4dp, formatUnitPrice, rateUsageForItem } from "./usageRater.js";
|
|
3
|
+
import { priceBasisOf } from "./lineAmounts.js";
|
|
2
4
|
const EMPTY_RESULT = {
|
|
3
5
|
lines: [],
|
|
4
6
|
matchedUoms: [],
|
|
@@ -15,7 +17,9 @@ function nonNullUomItems(items) {
|
|
|
15
17
|
}
|
|
16
18
|
return out;
|
|
17
19
|
}
|
|
18
|
-
async function fetchUsageAggregate(em, account, period) {
|
|
20
|
+
async function fetchUsageAggregate(em, account, period, after) {
|
|
21
|
+
const params = [account.tenantId, account.organizationId, account.id, period.periodEnd];
|
|
22
|
+
if (after) params.push(after);
|
|
19
23
|
const rows = await em.execute(
|
|
20
24
|
`SELECT uom_code,
|
|
21
25
|
SUM(quantity)::text AS total_quantity,
|
|
@@ -27,13 +31,14 @@ async function fetchUsageAggregate(em, account, period) {
|
|
|
27
31
|
AND deleted_at IS NULL
|
|
28
32
|
AND rated_in_bill_run_id IS NULL
|
|
29
33
|
AND period_end <= ?
|
|
34
|
+
${after ? "AND period_end > ?" : ""}
|
|
30
35
|
GROUP BY uom_code`,
|
|
31
|
-
|
|
36
|
+
params
|
|
32
37
|
);
|
|
33
38
|
return rows ?? [];
|
|
34
39
|
}
|
|
35
|
-
async function processUsageForAccount(em, account, period, usageItems) {
|
|
36
|
-
const aggregateRows = await fetchUsageAggregate(em, account, period);
|
|
40
|
+
async function processUsageForAccount(em, account, period, usageItems, options = {}) {
|
|
41
|
+
const aggregateRows = await fetchUsageAggregate(em, account, period, options.after ?? null);
|
|
37
42
|
if (aggregateRows.length === 0) return EMPTY_RESULT;
|
|
38
43
|
const itemByUom = nonNullUomItems(usageItems);
|
|
39
44
|
const lines = [];
|
|
@@ -61,9 +66,12 @@ async function processUsageForAccount(em, account, period, usageItems) {
|
|
|
61
66
|
billingItemId: item.id,
|
|
62
67
|
billingType: "usage",
|
|
63
68
|
description: item.description,
|
|
64
|
-
unitPriceNet,
|
|
69
|
+
unitPrice: unitPriceNet,
|
|
65
70
|
quantity: format4dp(totalQuantity),
|
|
66
|
-
|
|
71
|
+
lineAmount: rated.amount,
|
|
72
|
+
// Tiered rates are always net (P-D4); a simple rate keeps the item basis.
|
|
73
|
+
priceBasis: isSimpleFlat ? priceBasisOf(item.priceBasis) : "net",
|
|
74
|
+
taxRate: itemTaxRate(item),
|
|
67
75
|
metadataExtra: {
|
|
68
76
|
uom_code: uom,
|
|
69
77
|
usage_record_count: recordCount,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/lib/usageRunner.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type BillingAccount, type BillingItem } from '../data/entities'\nimport type { BillPeriod } from './billPeriod'\nimport type
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type BillingAccount, type BillingItem } from '../data/entities'\nimport type { BillPeriod } from './billPeriod'\nimport { itemTaxRate, type InvoiceLineDescriptor } from './invoiceWriter'\nimport { format4dp, formatUnitPrice, rateUsageForItem, type UsageRate } from './usageRater'\nimport { priceBasisOf } from './lineAmounts'\n\n/**\n * Per-account usage processing \u2014 Phase 3.\n *\n * ## Scale design\n *\n * This module's hot path runs once per account per Bill Run. For\n * telco / infra customers the spec's \"Shape B\" scenario projects\n * **millions of usage records per account per cycle**. A naive\n * `em.find(BillingAccountUsage, \u2026)` materialises every row in JS\n * memory before we can sum, which OOMs on those tenants.\n *\n * Phase 4f refactor: replace the load-all-then-aggregate path with a\n * single Postgres `GROUP BY uom_code` aggregate. Memory is now bounded\n * by `O(distinct uom_codes per account)` (a small number, typically\n * 1-20) rather than `O(records)`. The corresponding \"mark as rated\"\n * UPDATE happens engine-side via a `uom_code IN (\u2026)` predicate so we\n * never enumerate ids into memory either.\n *\n * ## Spec behaviour\n *\n * Selects all unrated usage records whose `period_end <= bill_period_end`,\n * groups them by `uom_code`, matches each group against the account's\n * `type=usage` Billing Items (exact `uom_code` match per spec \u2014 NO\n * conversion), rates the aggregated quantity, and emits ONE invoice\n * line per matched item.\n *\n * Per-record `line_description` itemization is deferred to a follow-up\n * (would require a non-aggregate path; the engine keeps the invoice\n * clean with one aggregated line per item for now).\n *\n * Unmatched UoMs:\n * - When usage records exist for a `uom_code` that does NOT match\n * any active `type=usage` Billing Item, those records are LEFT\n * un-rated (`rated_in_bill_run_id` stays NULL) and the uom is\n * surfaced in `unmatchedUoms` so the account outcome carries it\n * as `success_with_warnings`. Operator's fix: add the missing\n * Billing Item, then trigger retry-failed.\n *\n * Mode handling:\n * - real: the engine marks `rated_in_bill_run_id` on records matching\n * the returned `matchedUoms` via a single bulk UPDATE.\n * - test / dry: the engine skips the UPDATE; same usage stays\n * consumable by a follow-up run.\n */\n\nexport type UsageRunResult = {\n /** Invoice line descriptors ready for `writeDraftInvoice`. */\n lines: InvoiceLineDescriptor[]\n /**\n * `uom_code`s the engine should mark as rated. The engine issues a\n * single `UPDATE ... WHERE uom_code IN (matchedUoms) AND <scope>`\n * after this returns \u2014 no id list materialisation.\n */\n matchedUoms: string[]\n /** UoMs present in usage records but missing from `type=usage` items. */\n unmatchedUoms: string[]\n /** Count of usage records rated this cycle (for the run summary). */\n ratedCount: number\n}\n\nconst EMPTY_RESULT: UsageRunResult = {\n lines: [],\n matchedUoms: [],\n unmatchedUoms: [],\n ratedCount: 0,\n}\n\nfunction nonNullUomItems(items: readonly BillingItem[]): Map<string, BillingItem> {\n const out = new Map<string, BillingItem>()\n for (const item of items) {\n if (item.type !== 'usage') continue\n if (!item.isActive || item.deletedAt) continue\n if (!item.uomCode) continue\n out.set(item.uomCode, item)\n }\n return out\n}\n\ntype UsageAggregateRow = {\n uom_code: string\n total_quantity: string\n record_count: string\n}\n\n/**\n * Single Postgres aggregate: bucket unrated usage records by `uom_code`,\n * sum the quantities, count the records. Returns one row per uom \u2014 a\n * small, bounded result set regardless of how many millions of records\n * the account actually has.\n *\n * The matching index is the partial index on\n * `(bill_account_id, uom_code, rated_in_bill_run_id, period_end)` shipped\n * in Phase 0's migration \u2014 both the filter and the GROUP BY land on\n * the same index.\n */\nasync function fetchUsageAggregate(\n em: EntityManager,\n account: BillingAccount,\n period: BillPeriod,\n after: Date | null,\n): Promise<UsageAggregateRow[]> {\n // `after` (Spec A): exclusive lower bound for multi-cycle test / dry runs,\n // which never mark records as rated \u2014 without it every simulated cycle\n // would re-count the usage an earlier simulated cycle already billed.\n const params: unknown[] = [account.tenantId, account.organizationId, account.id, period.periodEnd]\n if (after) params.push(after)\n const rows = (await em.execute(\n `SELECT uom_code,\n SUM(quantity)::text AS total_quantity,\n COUNT(*)::text AS record_count\n FROM billing_account_usage\n WHERE tenant_id = ?\n AND organization_id = ?\n AND bill_account_id = ?\n AND deleted_at IS NULL\n AND rated_in_bill_run_id IS NULL\n AND period_end <= ?\n ${after ? 'AND period_end > ?' : ''}\n GROUP BY uom_code`,\n params,\n )) as unknown as UsageAggregateRow[] | undefined\n return rows ?? []\n}\n\nexport async function processUsageForAccount(\n em: EntityManager,\n account: BillingAccount,\n period: BillPeriod,\n usageItems: readonly BillingItem[],\n options: { after?: Date | null } = {},\n): Promise<UsageRunResult> {\n const aggregateRows = await fetchUsageAggregate(em, account, period, options.after ?? null)\n if (aggregateRows.length === 0) return EMPTY_RESULT\n\n const itemByUom = nonNullUomItems(usageItems)\n\n const lines: InvoiceLineDescriptor[] = []\n const matchedUoms: string[] = []\n const unmatchedUoms: string[] = []\n let ratedCount = 0\n\n for (const row of aggregateRows) {\n const uom = row.uom_code\n const totalQuantity = Number.parseFloat(row.total_quantity)\n const recordCount = Number.parseInt(row.record_count, 10)\n\n const item = itemByUom.get(uom)\n if (!item) {\n unmatchedUoms.push(uom)\n continue\n }\n if (!Number.isFinite(totalQuantity) || totalQuantity === 0) continue\n\n const rated = rateUsageForItem(item, totalQuantity)\n if (!rated) {\n // Malformed rate_json on a passing-validation item is rare but\n // possible if an operator hand-edited it. Surface as unmatched\n // so the operator knows to look rather than silently dropping\n // revenue.\n unmatchedUoms.push(uom)\n continue\n }\n\n const isSimpleFlat = !('model' in (item.rateJson as Record<string, unknown>))\n const unitPriceNet = isSimpleFlat\n ? formatUnitPrice((item.rateJson as { unit_price: number }).unit_price)\n : '0'\n lines.push({\n billingItemId: item.id,\n billingType: 'usage',\n description: item.description,\n unitPrice: unitPriceNet,\n quantity: format4dp(totalQuantity),\n lineAmount: rated.amount,\n // Tiered rates are always net (P-D4); a simple rate keeps the item basis.\n priceBasis: isSimpleFlat ? priceBasisOf(item.priceBasis) : 'net',\n taxRate: itemTaxRate(item),\n metadataExtra: {\n uom_code: uom,\n usage_record_count: recordCount,\n usage_tier_breakdown: rated.breakdown,\n },\n })\n matchedUoms.push(uom)\n ratedCount += Number.isFinite(recordCount) ? recordCount : 0\n }\n\n return { lines, matchedUoms, unmatchedUoms, ratedCount }\n}\n\nexport { EMPTY_RESULT as __EMPTY_USAGE_RESULT }\n\n// Suppress unused-export warning for the type alias.\nexport type _UsageRateAlias = UsageRate\n"],
|
|
5
|
+
"mappings": "AAGA,SAAS,mBAA+C;AACxD,SAAS,WAAW,iBAAiB,wBAAwC;AAC7E,SAAS,oBAAoB;AA8D7B,MAAM,eAA+B;AAAA,EACnC,OAAO,CAAC;AAAA,EACR,aAAa,CAAC;AAAA,EACd,eAAe,CAAC;AAAA,EAChB,YAAY;AACd;AAEA,SAAS,gBAAgB,OAAyD;AAChF,QAAM,MAAM,oBAAI,IAAyB;AACzC,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,SAAS,QAAS;AAC3B,QAAI,CAAC,KAAK,YAAY,KAAK,UAAW;AACtC,QAAI,CAAC,KAAK,QAAS;AACnB,QAAI,IAAI,KAAK,SAAS,IAAI;AAAA,EAC5B;AACA,SAAO;AACT;AAmBA,eAAe,oBACb,IACA,SACA,QACA,OAC8B;AAI9B,QAAM,SAAoB,CAAC,QAAQ,UAAU,QAAQ,gBAAgB,QAAQ,IAAI,OAAO,SAAS;AACjG,MAAI,MAAO,QAAO,KAAK,KAAK;AAC5B,QAAM,OAAQ,MAAM,GAAG;AAAA,IACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAUK,QAAQ,uBAAuB,EAAE;AAAA;AAAA,IAEtC;AAAA,EACF;AACA,SAAO,QAAQ,CAAC;AAClB;AAEA,eAAsB,uBACpB,IACA,SACA,QACA,YACA,UAAmC,CAAC,GACX;AACzB,QAAM,gBAAgB,MAAM,oBAAoB,IAAI,SAAS,QAAQ,QAAQ,SAAS,IAAI;AAC1F,MAAI,cAAc,WAAW,EAAG,QAAO;AAEvC,QAAM,YAAY,gBAAgB,UAAU;AAE5C,QAAM,QAAiC,CAAC;AACxC,QAAM,cAAwB,CAAC;AAC/B,QAAM,gBAA0B,CAAC;AACjC,MAAI,aAAa;AAEjB,aAAW,OAAO,eAAe;AAC/B,UAAM,MAAM,IAAI;AAChB,UAAM,gBAAgB,OAAO,WAAW,IAAI,cAAc;AAC1D,UAAM,cAAc,OAAO,SAAS,IAAI,cAAc,EAAE;AAExD,UAAM,OAAO,UAAU,IAAI,GAAG;AAC9B,QAAI,CAAC,MAAM;AACT,oBAAc,KAAK,GAAG;AACtB;AAAA,IACF;AACA,QAAI,CAAC,OAAO,SAAS,aAAa,KAAK,kBAAkB,EAAG;AAE5D,UAAM,QAAQ,iBAAiB,MAAM,aAAa;AAClD,QAAI,CAAC,OAAO;AAKV,oBAAc,KAAK,GAAG;AACtB;AAAA,IACF;AAEA,UAAM,eAAe,EAAE,WAAY,KAAK;AACxC,UAAM,eAAe,eACjB,gBAAiB,KAAK,SAAoC,UAAU,IACpE;AACJ,UAAM,KAAK;AAAA,MACT,eAAe,KAAK;AAAA,MACpB,aAAa;AAAA,MACb,aAAa,KAAK;AAAA,MAClB,WAAW;AAAA,MACX,UAAU,UAAU,aAAa;AAAA,MACjC,YAAY,MAAM;AAAA;AAAA,MAElB,YAAY,eAAe,aAAa,KAAK,UAAU,IAAI;AAAA,MAC3D,SAAS,YAAY,IAAI;AAAA,MACzB,eAAe;AAAA,QACb,UAAU;AAAA,QACV,oBAAoB;AAAA,QACpB,sBAAsB,MAAM;AAAA,MAC9B;AAAA,IACF,CAAC;AACD,gBAAY,KAAK,GAAG;AACpB,kBAAc,OAAO,SAAS,WAAW,IAAI,cAAc;AAAA,EAC7D;AAEA,SAAO,EAAE,OAAO,aAAa,eAAe,WAAW;AACzD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|