@dainamite/billing 0.20.0 → 0.21.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 +90 -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 +46 -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 +69 -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/en.json +457 -21
- package/dist/modules/billing/i18n/pl.json +457 -21
- 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 +131 -0
- package/dist/modules/billing/lib/invoiceDelivery.js.map +7 -0
- package/dist/modules/billing/lib/invoiceLanguages.js +16 -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 +274 -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 +170 -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__/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 +189 -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 +199 -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 +193 -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 +58 -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 +79 -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 +654 -5
- 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/en.json +457 -21
- package/src/modules/billing/i18n/pl.json +457 -21
- 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 +197 -0
- package/src/modules/billing/lib/invoiceLanguages.ts +34 -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 +328 -0
- package/src/modules/billing/lib/pdf/invoicePdf.ts +216 -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,356 @@
|
|
|
1
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
2
|
+
import { BillingAllocation } from "../data/entities.js";
|
|
3
|
+
function toCents(value) {
|
|
4
|
+
const n = typeof value === "number" ? value : Number.parseFloat(value ?? "0");
|
|
5
|
+
return Number.isFinite(n) ? Math.round(n * 100) : 0;
|
|
6
|
+
}
|
|
7
|
+
function fromCents(cents) {
|
|
8
|
+
return (cents / 100).toFixed(4);
|
|
9
|
+
}
|
|
10
|
+
function sortOpenReceivables(rows) {
|
|
11
|
+
return [...rows].sort((a, b) => {
|
|
12
|
+
if (a.dueDate !== b.dueDate) {
|
|
13
|
+
if (!a.dueDate) return 1;
|
|
14
|
+
if (!b.dueDate) return -1;
|
|
15
|
+
return a.dueDate < b.dueDate ? -1 : 1;
|
|
16
|
+
}
|
|
17
|
+
if (a.issueDate !== b.issueDate) {
|
|
18
|
+
if (!a.issueDate) return 1;
|
|
19
|
+
if (!b.issueDate) return -1;
|
|
20
|
+
return a.issueDate < b.issueDate ? -1 : 1;
|
|
21
|
+
}
|
|
22
|
+
return a.number.localeCompare(b.number);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function planAllocation(amountCents, receivables) {
|
|
26
|
+
let remaining = Math.max(0, amountCents);
|
|
27
|
+
const allocations = [];
|
|
28
|
+
for (const receivable of receivables) {
|
|
29
|
+
if (remaining <= 0) break;
|
|
30
|
+
const take = Math.min(remaining, Math.max(0, receivable.outstandingCents));
|
|
31
|
+
if (take <= 0) continue;
|
|
32
|
+
allocations.push({ invoiceId: receivable.id, amountCents: take });
|
|
33
|
+
remaining -= take;
|
|
34
|
+
}
|
|
35
|
+
return { allocations, remainderCents: remaining };
|
|
36
|
+
}
|
|
37
|
+
function recomputeReceivable(state) {
|
|
38
|
+
const { totalGrossCents: gross, paidCents: paid, creditedCents: credited } = state;
|
|
39
|
+
const outstandingCents = Math.max(0, gross - paid - credited);
|
|
40
|
+
if (state.status === "draft") return { paidCents: paid, creditedCents: credited, outstandingCents, status: "draft" };
|
|
41
|
+
let status;
|
|
42
|
+
if (gross > 0 && outstandingCents === 0) status = credited >= gross ? "credited" : "paid";
|
|
43
|
+
else if (paid + credited > 0) status = "partially_paid";
|
|
44
|
+
else status = "posted";
|
|
45
|
+
return { paidCents: paid, creditedCents: credited, outstandingCents, status };
|
|
46
|
+
}
|
|
47
|
+
function paymentStatusFor(payment) {
|
|
48
|
+
if (payment.status === "ignored" || payment.status === "converted_to_topup") return payment.status;
|
|
49
|
+
if (!payment.billAccountId) return "unmatched";
|
|
50
|
+
if (payment.unallocatedCents <= 0) return "allocated";
|
|
51
|
+
if (payment.unallocatedCents < payment.amountCents) return "partially_allocated";
|
|
52
|
+
return "matched";
|
|
53
|
+
}
|
|
54
|
+
function emptyEffects() {
|
|
55
|
+
return { created: [], removed: [], invoiceStatusChanges: [] };
|
|
56
|
+
}
|
|
57
|
+
function mergeEffects(target, extra) {
|
|
58
|
+
target.created.push(...extra.created);
|
|
59
|
+
target.removed.push(...extra.removed);
|
|
60
|
+
target.invoiceStatusChanges.push(...extra.invoiceStatusChanges);
|
|
61
|
+
return target;
|
|
62
|
+
}
|
|
63
|
+
const RECEIVABLE_TYPES = ["invoice", "debit_memo"];
|
|
64
|
+
const OPEN_STATUSES = ["posted", "partially_paid"];
|
|
65
|
+
function isoDay(value) {
|
|
66
|
+
if (!value) return null;
|
|
67
|
+
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
68
|
+
return String(value).slice(0, 10);
|
|
69
|
+
}
|
|
70
|
+
async function loadReceivable(tem, scope, invoiceId) {
|
|
71
|
+
const rows = await tem.execute(
|
|
72
|
+
`SELECT id, bill_account_id, currency_code, status, document_type, test_run, total_gross, outstanding,
|
|
73
|
+
due_date, issue_date, number
|
|
74
|
+
FROM billing_invoices
|
|
75
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL`,
|
|
76
|
+
[invoiceId, scope.tenantId, scope.organizationId]
|
|
77
|
+
);
|
|
78
|
+
return rows?.[0] ?? null;
|
|
79
|
+
}
|
|
80
|
+
async function loadSource(tem, scope, source) {
|
|
81
|
+
const sql = source.type === "payment" ? `SELECT id, bill_account_id, currency_code, unallocated_amount AS unallocated, status
|
|
82
|
+
FROM billing_payments
|
|
83
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL` : `SELECT id, bill_account_id, currency_code, outstanding AS unallocated, status
|
|
84
|
+
FROM billing_invoices
|
|
85
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL
|
|
86
|
+
AND document_type = 'credit_memo'`;
|
|
87
|
+
const rows = await tem.execute(sql, [source.id, scope.tenantId, scope.organizationId]);
|
|
88
|
+
return rows?.[0] ?? null;
|
|
89
|
+
}
|
|
90
|
+
async function adjustSourceUnallocated(tem, scope, source, deltaCents) {
|
|
91
|
+
const amount = fromCents(Math.abs(deltaCents));
|
|
92
|
+
if (source.type === "payment") {
|
|
93
|
+
const rows2 = await tem.execute(
|
|
94
|
+
deltaCents >= 0 ? `UPDATE billing_payments
|
|
95
|
+
SET unallocated_amount = unallocated_amount - ?::numeric, updated_at = now()
|
|
96
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ? AND unallocated_amount >= ?::numeric
|
|
97
|
+
RETURNING id` : `UPDATE billing_payments
|
|
98
|
+
SET unallocated_amount = LEAST(amount, unallocated_amount + ?::numeric), updated_at = now()
|
|
99
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ?
|
|
100
|
+
RETURNING id`,
|
|
101
|
+
deltaCents >= 0 ? [amount, source.id, scope.tenantId, scope.organizationId, amount] : [amount, source.id, scope.tenantId, scope.organizationId]
|
|
102
|
+
);
|
|
103
|
+
if (!rows2?.length) return false;
|
|
104
|
+
await tem.execute(
|
|
105
|
+
`UPDATE billing_payments
|
|
106
|
+
SET status = CASE
|
|
107
|
+
WHEN unallocated_amount <= 0 THEN 'allocated'
|
|
108
|
+
WHEN unallocated_amount < amount THEN 'partially_allocated'
|
|
109
|
+
ELSE 'matched' END
|
|
110
|
+
WHERE id = ? AND status IN ('matched', 'partially_allocated', 'allocated')`,
|
|
111
|
+
[source.id]
|
|
112
|
+
);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const rows = await tem.execute(
|
|
116
|
+
deltaCents >= 0 ? `UPDATE billing_invoices
|
|
117
|
+
SET outstanding = outstanding - ?::numeric,
|
|
118
|
+
status = CASE WHEN outstanding - ?::numeric <= 0 THEN 'paid' ELSE 'posted' END,
|
|
119
|
+
updated_at = now()
|
|
120
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ? AND document_type = 'credit_memo'
|
|
121
|
+
AND outstanding >= ?::numeric
|
|
122
|
+
RETURNING id` : `UPDATE billing_invoices
|
|
123
|
+
SET outstanding = LEAST(total_gross, outstanding + ?::numeric), status = 'posted', updated_at = now()
|
|
124
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ? AND document_type = 'credit_memo'
|
|
125
|
+
RETURNING id`,
|
|
126
|
+
deltaCents >= 0 ? [amount, amount, source.id, scope.tenantId, scope.organizationId, amount] : [amount, source.id, scope.tenantId, scope.organizationId]
|
|
127
|
+
);
|
|
128
|
+
return Boolean(rows?.length);
|
|
129
|
+
}
|
|
130
|
+
async function recomputeInvoice(tem, scope, invoiceId) {
|
|
131
|
+
const invoice = await loadReceivable(tem, scope, invoiceId);
|
|
132
|
+
if (!invoice || !RECEIVABLE_TYPES.includes(invoice.document_type)) return null;
|
|
133
|
+
const sums = await tem.execute(
|
|
134
|
+
`SELECT COALESCE(SUM(amount) FILTER (WHERE source_type = 'payment'), 0) AS paid,
|
|
135
|
+
COALESCE(SUM(amount) FILTER (WHERE source_type = 'credit_memo'), 0) AS credited
|
|
136
|
+
FROM billing_allocations
|
|
137
|
+
WHERE invoice_id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL`,
|
|
138
|
+
[invoiceId, scope.tenantId, scope.organizationId]
|
|
139
|
+
);
|
|
140
|
+
const next = recomputeReceivable({
|
|
141
|
+
status: invoice.status,
|
|
142
|
+
totalGrossCents: toCents(invoice.total_gross),
|
|
143
|
+
paidCents: toCents(sums?.[0]?.paid),
|
|
144
|
+
creditedCents: toCents(sums?.[0]?.credited)
|
|
145
|
+
});
|
|
146
|
+
await tem.execute(
|
|
147
|
+
`UPDATE billing_invoices
|
|
148
|
+
SET paid_total = ?::numeric, credited_total = ?::numeric, outstanding = ?::numeric, status = ?, updated_at = now()
|
|
149
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ?`,
|
|
150
|
+
[
|
|
151
|
+
fromCents(next.paidCents),
|
|
152
|
+
fromCents(next.creditedCents),
|
|
153
|
+
fromCents(next.outstandingCents),
|
|
154
|
+
next.status,
|
|
155
|
+
invoiceId,
|
|
156
|
+
scope.tenantId,
|
|
157
|
+
scope.organizationId
|
|
158
|
+
]
|
|
159
|
+
);
|
|
160
|
+
return next.status !== invoice.status ? { invoiceId, from: invoice.status, to: next.status } : null;
|
|
161
|
+
}
|
|
162
|
+
async function allocate(tem, scope, params) {
|
|
163
|
+
const effects = emptyEffects();
|
|
164
|
+
if (params.amountCents <= 0) {
|
|
165
|
+
throw new CrudHttpError(400, { error: "Allocation amount must be positive", code: "billing.allocation.invalid_amount" });
|
|
166
|
+
}
|
|
167
|
+
const [source, invoice] = await Promise.all([
|
|
168
|
+
loadSource(tem, scope, params.source),
|
|
169
|
+
loadReceivable(tem, scope, params.invoiceId)
|
|
170
|
+
]);
|
|
171
|
+
if (!source) throw new CrudHttpError(404, { error: "Payment source not found", code: "billing.allocation.source_missing" });
|
|
172
|
+
if (!invoice) throw new CrudHttpError(404, { error: "Invoice not found", code: "billing.allocation.invoice_missing" });
|
|
173
|
+
if (!source.bill_account_id || source.bill_account_id !== invoice.bill_account_id) {
|
|
174
|
+
throw new CrudHttpError(409, {
|
|
175
|
+
error: "Payment and invoice belong to different accounts",
|
|
176
|
+
code: "billing.allocation.account_mismatch"
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (source.currency_code !== invoice.currency_code) {
|
|
180
|
+
throw new CrudHttpError(409, { error: "Currencies differ", code: "billing.allocation.currency_mismatch" });
|
|
181
|
+
}
|
|
182
|
+
if (invoice.test_run || !RECEIVABLE_TYPES.includes(invoice.document_type) || !OPEN_STATUSES.includes(invoice.status)) {
|
|
183
|
+
throw new CrudHttpError(409, { error: "Invoice is not open for payment", code: "billing.allocation.invoice_not_open" });
|
|
184
|
+
}
|
|
185
|
+
if (params.amountCents > toCents(invoice.outstanding)) {
|
|
186
|
+
throw new CrudHttpError(409, {
|
|
187
|
+
error: "Allocation exceeds the invoice outstanding amount",
|
|
188
|
+
code: "billing.allocation.exceeds_outstanding"
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const decremented = await adjustSourceUnallocated(tem, scope, params.source, params.amountCents);
|
|
192
|
+
if (!decremented) {
|
|
193
|
+
throw new CrudHttpError(409, {
|
|
194
|
+
error: "Not enough unallocated amount",
|
|
195
|
+
code: "billing.allocation.insufficient_unallocated"
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const now = /* @__PURE__ */ new Date();
|
|
199
|
+
const row = tem.create(BillingAllocation, {
|
|
200
|
+
organizationId: scope.organizationId,
|
|
201
|
+
tenantId: scope.tenantId,
|
|
202
|
+
sourceType: params.source.type,
|
|
203
|
+
sourceId: params.source.id,
|
|
204
|
+
invoiceId: params.invoiceId,
|
|
205
|
+
billAccountId: invoice.bill_account_id,
|
|
206
|
+
amount: fromCents(params.amountCents),
|
|
207
|
+
currencyCode: invoice.currency_code,
|
|
208
|
+
allocatedBy: params.by,
|
|
209
|
+
userId: params.userId ?? null,
|
|
210
|
+
createdAt: now,
|
|
211
|
+
updatedAt: now
|
|
212
|
+
});
|
|
213
|
+
tem.persist(row);
|
|
214
|
+
await tem.flush();
|
|
215
|
+
effects.created.push({
|
|
216
|
+
allocationId: row.id,
|
|
217
|
+
sourceType: params.source.type,
|
|
218
|
+
sourceId: params.source.id,
|
|
219
|
+
invoiceId: params.invoiceId,
|
|
220
|
+
amount: row.amount
|
|
221
|
+
});
|
|
222
|
+
const change = await recomputeInvoice(tem, scope, params.invoiceId);
|
|
223
|
+
if (change) effects.invoiceStatusChanges.push(change);
|
|
224
|
+
return effects;
|
|
225
|
+
}
|
|
226
|
+
async function unallocate(tem, scope, params) {
|
|
227
|
+
const effects = emptyEffects();
|
|
228
|
+
const rows = await tem.execute(
|
|
229
|
+
`UPDATE billing_allocations
|
|
230
|
+
SET deleted_at = now(), updated_at = now(), deallocated_by_user_id = ?
|
|
231
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL
|
|
232
|
+
RETURNING id, source_type, source_id, invoice_id, amount`,
|
|
233
|
+
[params.userId ?? null, params.allocationId, scope.tenantId, scope.organizationId]
|
|
234
|
+
);
|
|
235
|
+
const removed = rows?.[0];
|
|
236
|
+
if (!removed) throw new CrudHttpError(404, { error: "Allocation not found", code: "billing.allocation.not_found" });
|
|
237
|
+
await adjustSourceUnallocated(tem, scope, { type: removed.source_type, id: removed.source_id }, -toCents(removed.amount));
|
|
238
|
+
effects.removed.push({
|
|
239
|
+
allocationId: removed.id,
|
|
240
|
+
sourceType: removed.source_type,
|
|
241
|
+
sourceId: removed.source_id,
|
|
242
|
+
invoiceId: removed.invoice_id,
|
|
243
|
+
amount: removed.amount
|
|
244
|
+
});
|
|
245
|
+
const change = await recomputeInvoice(tem, scope, removed.invoice_id);
|
|
246
|
+
if (change) effects.invoiceStatusChanges.push(change);
|
|
247
|
+
return effects;
|
|
248
|
+
}
|
|
249
|
+
async function loadOpenReceivables(tem, scope, account, onlyIds) {
|
|
250
|
+
const params = [scope.tenantId, scope.organizationId, account.id, account.currencyCode];
|
|
251
|
+
let idFilter = "";
|
|
252
|
+
if (onlyIds && onlyIds.length > 0) {
|
|
253
|
+
idFilter = ` AND id = ANY(?::uuid[])`;
|
|
254
|
+
params.push(`{${onlyIds.join(",")}}`);
|
|
255
|
+
}
|
|
256
|
+
const rows = await tem.execute(
|
|
257
|
+
`SELECT id, outstanding, due_date, issue_date, number
|
|
258
|
+
FROM billing_invoices
|
|
259
|
+
WHERE tenant_id = ? AND organization_id = ? AND bill_account_id = ? AND currency_code = ?
|
|
260
|
+
AND deleted_at IS NULL AND test_run = false
|
|
261
|
+
AND document_type IN ('invoice', 'debit_memo')
|
|
262
|
+
AND status IN ('posted', 'partially_paid')
|
|
263
|
+
AND outstanding > 0${idFilter}`,
|
|
264
|
+
params
|
|
265
|
+
);
|
|
266
|
+
return sortOpenReceivables(
|
|
267
|
+
(rows ?? []).map((row) => ({
|
|
268
|
+
id: row.id,
|
|
269
|
+
outstandingCents: toCents(row.outstanding),
|
|
270
|
+
dueDate: isoDay(row.due_date),
|
|
271
|
+
issueDate: isoDay(row.issue_date),
|
|
272
|
+
number: row.number
|
|
273
|
+
}))
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
async function loadCreditSources(tem, scope, account) {
|
|
277
|
+
const rows = await tem.execute(
|
|
278
|
+
`SELECT 'payment' AS source_type, id, unallocated_amount AS available, received_at AS at, created_at
|
|
279
|
+
FROM billing_payments
|
|
280
|
+
WHERE tenant_id = ? AND organization_id = ? AND bill_account_id = ? AND currency_code = ?
|
|
281
|
+
AND deleted_at IS NULL AND status IN ('matched', 'partially_allocated') AND unallocated_amount > 0
|
|
282
|
+
UNION ALL
|
|
283
|
+
SELECT 'credit_memo' AS source_type, id, outstanding AS available, issue_date AS at, created_at
|
|
284
|
+
FROM billing_invoices
|
|
285
|
+
WHERE tenant_id = ? AND organization_id = ? AND bill_account_id = ? AND currency_code = ?
|
|
286
|
+
AND deleted_at IS NULL AND test_run = false AND document_type = 'credit_memo'
|
|
287
|
+
AND status = 'posted' AND outstanding > 0
|
|
288
|
+
ORDER BY at ASC, created_at ASC`,
|
|
289
|
+
[
|
|
290
|
+
scope.tenantId,
|
|
291
|
+
scope.organizationId,
|
|
292
|
+
account.id,
|
|
293
|
+
account.currencyCode,
|
|
294
|
+
scope.tenantId,
|
|
295
|
+
scope.organizationId,
|
|
296
|
+
account.id,
|
|
297
|
+
account.currencyCode
|
|
298
|
+
]
|
|
299
|
+
);
|
|
300
|
+
return (rows ?? []).map((row) => ({
|
|
301
|
+
source: { type: row.source_type, id: row.id },
|
|
302
|
+
availableCents: toCents(row.available)
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
async function applyAccountCredit(tem, scope, account, opts = {}) {
|
|
306
|
+
const effects = emptyEffects();
|
|
307
|
+
let sources = await loadCreditSources(tem, scope, account);
|
|
308
|
+
if (opts.onlySources) {
|
|
309
|
+
const wanted = new Set(opts.onlySources.map((s) => `${s.type}:${s.id}`));
|
|
310
|
+
sources = sources.filter((s) => wanted.has(`${s.source.type}:${s.source.id}`));
|
|
311
|
+
}
|
|
312
|
+
if (sources.length === 0) return effects;
|
|
313
|
+
const receivables = await loadOpenReceivables(tem, scope, account);
|
|
314
|
+
for (const credit of sources) {
|
|
315
|
+
let available = credit.availableCents;
|
|
316
|
+
for (const receivable of receivables) {
|
|
317
|
+
if (available <= 0) break;
|
|
318
|
+
const take = Math.min(available, receivable.outstandingCents);
|
|
319
|
+
if (take <= 0) continue;
|
|
320
|
+
mergeEffects(
|
|
321
|
+
effects,
|
|
322
|
+
await allocate(tem, scope, {
|
|
323
|
+
source: credit.source,
|
|
324
|
+
invoiceId: receivable.id,
|
|
325
|
+
amountCents: take,
|
|
326
|
+
by: opts.by ?? "credit"
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
receivable.outstandingCents -= take;
|
|
330
|
+
available -= take;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return effects;
|
|
334
|
+
}
|
|
335
|
+
async function getAccountCredit(em, scope, account) {
|
|
336
|
+
const sources = await loadCreditSources(em, scope, account);
|
|
337
|
+
return fromCents(sources.reduce((sum, s) => sum + s.availableCents, 0));
|
|
338
|
+
}
|
|
339
|
+
export {
|
|
340
|
+
allocate,
|
|
341
|
+
applyAccountCredit,
|
|
342
|
+
emptyEffects,
|
|
343
|
+
fromCents,
|
|
344
|
+
getAccountCredit,
|
|
345
|
+
loadCreditSources,
|
|
346
|
+
loadOpenReceivables,
|
|
347
|
+
mergeEffects,
|
|
348
|
+
paymentStatusFor,
|
|
349
|
+
planAllocation,
|
|
350
|
+
recomputeInvoice,
|
|
351
|
+
recomputeReceivable,
|
|
352
|
+
sortOpenReceivables,
|
|
353
|
+
toCents,
|
|
354
|
+
unallocate
|
|
355
|
+
};
|
|
356
|
+
//# sourceMappingURL=allocation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/allocation.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { BillingAllocation } from '../data/entities'\nimport type { AllocatedBy, AllocationSourceType } from '../data/validators'\n\n/**\n * Allocation service (Spec C; generalised for Spec D credit memos).\n *\n * An allocation moves money from a SOURCE (`payment` \u2192 `billing_payments`, or\n * `credit_memo` \u2192 a posted credit memo in `billing_invoices`) to a receivable\n * (`invoice` / `debit_memo`). Rules:\n *\n * - Receivable amounts are RECOMPUTED from \u03A3 active allocations, never\n * incremented: `paid_total` (payment sources), `credited_total` (credit\n * memo sources), `outstanding = gross \u2212 paid \u2212 credited`, status\n * `posted | partially_paid | paid | credited` (P-D2, Spec D P-D3).\n * - A source's unallocated amount (payment `unallocated_amount`, credit memo\n * `outstanding`) is decremented with a guarded UPDATE, so it can never go\n * negative even if a caller forgets the account lock.\n * - Callers hold `withAccountLock` (P-D3); every function here takes the\n * transactional EntityManager of that lock.\n * - Unallocated money of an account is ACCOUNT CREDIT; `applyAccountCredit`\n * spends it on open receivables, oldest due date first (D12, P-D5).\n *\n * Money is handled in integer cents internally (2 dp documents).\n */\n\n// \u2500\u2500\u2500 Pure helpers \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\nexport function toCents(value: string | number | null | undefined): number {\n const n = typeof value === 'number' ? value : Number.parseFloat(value ?? '0')\n return Number.isFinite(n) ? Math.round(n * 100) : 0\n}\n\nexport function fromCents(cents: number): string {\n return (cents / 100).toFixed(4)\n}\n\nexport type OpenReceivable = {\n id: string\n outstandingCents: number\n dueDate: string | null\n issueDate: string | null\n number: string\n}\n\n/** Oldest due date first (nulls last), then issue date, then number. */\nexport function sortOpenReceivables<T extends Omit<OpenReceivable, 'outstandingCents'>>(rows: T[]): T[] {\n return [...rows].sort((a, b) => {\n if (a.dueDate !== b.dueDate) {\n if (!a.dueDate) return 1\n if (!b.dueDate) return -1\n return a.dueDate < b.dueDate ? -1 : 1\n }\n if (a.issueDate !== b.issueDate) {\n if (!a.issueDate) return 1\n if (!b.issueDate) return -1\n return a.issueDate < b.issueDate ? -1 : 1\n }\n return a.number.localeCompare(b.number)\n })\n}\n\n/** Spreads `amountCents` over receivables in the given order, never above outstanding. */\nexport function planAllocation(\n amountCents: number,\n receivables: ReadonlyArray<Pick<OpenReceivable, 'id' | 'outstandingCents'>>,\n): { allocations: Array<{ invoiceId: string; amountCents: number }>; remainderCents: number } {\n let remaining = Math.max(0, amountCents)\n const allocations: Array<{ invoiceId: string; amountCents: number }> = []\n for (const receivable of receivables) {\n if (remaining <= 0) break\n const take = Math.min(remaining, Math.max(0, receivable.outstandingCents))\n if (take <= 0) continue\n allocations.push({ invoiceId: receivable.id, amountCents: take })\n remaining -= take\n }\n return { allocations, remainderCents: remaining }\n}\n\nexport type ReceivableState = {\n status: string\n totalGrossCents: number\n paidCents: number\n creditedCents: number\n}\n\n/** Paid / credited / outstanding / status of a receivable from its allocation sums. */\nexport function recomputeReceivable(state: ReceivableState): {\n paidCents: number\n creditedCents: number\n outstandingCents: number\n status: string\n} {\n const { totalGrossCents: gross, paidCents: paid, creditedCents: credited } = state\n const outstandingCents = Math.max(0, gross - paid - credited)\n if (state.status === 'draft') return { paidCents: paid, creditedCents: credited, outstandingCents, status: 'draft' }\n let status: string\n if (gross > 0 && outstandingCents === 0) status = credited >= gross ? 'credited' : 'paid'\n else if (paid + credited > 0) status = 'partially_paid'\n else status = 'posted'\n return { paidCents: paid, creditedCents: credited, outstandingCents, status }\n}\n\n/** Payment status from its allocation state (terminal / unmatched states are kept). */\nexport function paymentStatusFor(payment: {\n status: string\n billAccountId: string | null | undefined\n amountCents: number\n unallocatedCents: number\n}): string {\n if (payment.status === 'ignored' || payment.status === 'converted_to_topup') return payment.status\n if (!payment.billAccountId) return 'unmatched'\n if (payment.unallocatedCents <= 0) return 'allocated'\n if (payment.unallocatedCents < payment.amountCents) return 'partially_allocated'\n return 'matched'\n}\n\n// \u2500\u2500\u2500 Effects (emitted by callers after commit) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type InvoiceStatusChange = { invoiceId: string; from: string; to: string }\n\nexport type AllocationEffects = {\n created: Array<{ allocationId: string; sourceType: AllocationSourceType; sourceId: string; invoiceId: string; amount: string }>\n removed: Array<{ allocationId: string; sourceType: AllocationSourceType; sourceId: string; invoiceId: string; amount: string }>\n invoiceStatusChanges: InvoiceStatusChange[]\n}\n\nexport function emptyEffects(): AllocationEffects {\n return { created: [], removed: [], invoiceStatusChanges: [] }\n}\n\nexport function mergeEffects(target: AllocationEffects, extra: AllocationEffects): AllocationEffects {\n target.created.push(...extra.created)\n target.removed.push(...extra.removed)\n target.invoiceStatusChanges.push(...extra.invoiceStatusChanges)\n return target\n}\n\n// \u2500\u2500\u2500 DB service (inside withAccountLock) \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 type ArScope = { tenantId: string; organizationId: string }\n\nexport type AllocationSource = { type: AllocationSourceType; id: string }\n\ntype ReceivableRow = {\n id: string\n bill_account_id: string\n currency_code: string\n status: string\n document_type: string\n test_run: boolean\n total_gross: string\n outstanding: string\n due_date: string | null\n issue_date: string | null\n number: string\n}\n\ntype SourceRow = {\n id: string\n bill_account_id: string | null\n currency_code: string\n unallocated: string\n status: string\n}\n\nconst RECEIVABLE_TYPES = ['invoice', 'debit_memo']\nconst OPEN_STATUSES = ['posted', 'partially_paid']\n\nfunction isoDay(value: unknown): string | null {\n if (!value) return null\n if (value instanceof Date) return value.toISOString().slice(0, 10)\n return String(value).slice(0, 10)\n}\n\nasync function loadReceivable(tem: EntityManager, scope: ArScope, invoiceId: string): Promise<ReceivableRow | null> {\n const rows = (await tem.execute(\n `SELECT id, bill_account_id, currency_code, status, document_type, test_run, total_gross, outstanding,\n due_date, issue_date, number\n FROM billing_invoices\n WHERE id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL`,\n [invoiceId, scope.tenantId, scope.organizationId],\n )) as unknown as ReceivableRow[]\n return rows?.[0] ?? null\n}\n\nasync function loadSource(tem: EntityManager, scope: ArScope, source: AllocationSource): Promise<SourceRow | null> {\n const sql =\n source.type === 'payment'\n ? `SELECT id, bill_account_id, currency_code, unallocated_amount AS unallocated, status\n FROM billing_payments\n WHERE id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL`\n : `SELECT id, bill_account_id, currency_code, outstanding AS unallocated, status\n FROM billing_invoices\n WHERE id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL\n AND document_type = 'credit_memo'`\n const rows = (await tem.execute(sql, [source.id, scope.tenantId, scope.organizationId])) as unknown as SourceRow[]\n return rows?.[0] ?? null\n}\n\n/** Guarded decrement (negative `deltaCents` restores). 0 rows \u2192 insufficient. */\nasync function adjustSourceUnallocated(\n tem: EntityManager,\n scope: ArScope,\n source: AllocationSource,\n deltaCents: number,\n): Promise<boolean> {\n const amount = fromCents(Math.abs(deltaCents))\n if (source.type === 'payment') {\n const rows = (await tem.execute(\n deltaCents >= 0\n ? `UPDATE billing_payments\n SET unallocated_amount = unallocated_amount - ?::numeric, updated_at = now()\n WHERE id = ? AND tenant_id = ? AND organization_id = ? AND unallocated_amount >= ?::numeric\n RETURNING id`\n : `UPDATE billing_payments\n SET unallocated_amount = LEAST(amount, unallocated_amount + ?::numeric), updated_at = now()\n WHERE id = ? AND tenant_id = ? AND organization_id = ?\n RETURNING id`,\n deltaCents >= 0\n ? [amount, source.id, scope.tenantId, scope.organizationId, amount]\n : [amount, source.id, scope.tenantId, scope.organizationId],\n )) as unknown as Array<{ id: string }>\n if (!rows?.length) return false\n await tem.execute(\n `UPDATE billing_payments\n SET status = CASE\n WHEN unallocated_amount <= 0 THEN 'allocated'\n WHEN unallocated_amount < amount THEN 'partially_allocated'\n ELSE 'matched' END\n WHERE id = ? AND status IN ('matched', 'partially_allocated', 'allocated')`,\n [source.id],\n )\n return true\n }\n // Credit memo: its `outstanding` is the unallocated credit (Spec D, P-D5).\n const rows = (await tem.execute(\n deltaCents >= 0\n ? `UPDATE billing_invoices\n SET outstanding = outstanding - ?::numeric,\n status = CASE WHEN outstanding - ?::numeric <= 0 THEN 'paid' ELSE 'posted' END,\n updated_at = now()\n WHERE id = ? AND tenant_id = ? AND organization_id = ? AND document_type = 'credit_memo'\n AND outstanding >= ?::numeric\n RETURNING id`\n : `UPDATE billing_invoices\n SET outstanding = LEAST(total_gross, outstanding + ?::numeric), status = 'posted', updated_at = now()\n WHERE id = ? AND tenant_id = ? AND organization_id = ? AND document_type = 'credit_memo'\n RETURNING id`,\n deltaCents >= 0\n ? [amount, amount, source.id, scope.tenantId, scope.organizationId, amount]\n : [amount, source.id, scope.tenantId, scope.organizationId],\n )) as unknown as Array<{ id: string }>\n return Boolean(rows?.length)\n}\n\n/** Recomputes paid / credited / outstanding / status of a receivable from its active allocations. */\nexport async function recomputeInvoice(\n tem: EntityManager,\n scope: ArScope,\n invoiceId: string,\n): Promise<InvoiceStatusChange | null> {\n const invoice = await loadReceivable(tem, scope, invoiceId)\n if (!invoice || !RECEIVABLE_TYPES.includes(invoice.document_type)) return null\n const sums = (await tem.execute(\n `SELECT COALESCE(SUM(amount) FILTER (WHERE source_type = 'payment'), 0) AS paid,\n COALESCE(SUM(amount) FILTER (WHERE source_type = 'credit_memo'), 0) AS credited\n FROM billing_allocations\n WHERE invoice_id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL`,\n [invoiceId, scope.tenantId, scope.organizationId],\n )) as unknown as Array<{ paid: string; credited: string }>\n const next = recomputeReceivable({\n status: invoice.status,\n totalGrossCents: toCents(invoice.total_gross),\n paidCents: toCents(sums?.[0]?.paid),\n creditedCents: toCents(sums?.[0]?.credited),\n })\n await tem.execute(\n `UPDATE billing_invoices\n SET paid_total = ?::numeric, credited_total = ?::numeric, outstanding = ?::numeric, status = ?, updated_at = now()\n WHERE id = ? AND tenant_id = ? AND organization_id = ?`,\n [\n fromCents(next.paidCents),\n fromCents(next.creditedCents),\n fromCents(next.outstandingCents),\n next.status,\n invoiceId,\n scope.tenantId,\n scope.organizationId,\n ],\n )\n return next.status !== invoice.status ? { invoiceId, from: invoice.status, to: next.status } : null\n}\n\nexport async function allocate(\n tem: EntityManager,\n scope: ArScope,\n params: { source: AllocationSource; invoiceId: string; amountCents: number; by: AllocatedBy; userId?: string | null },\n): Promise<AllocationEffects> {\n const effects = emptyEffects()\n if (params.amountCents <= 0) {\n throw new CrudHttpError(400, { error: 'Allocation amount must be positive', code: 'billing.allocation.invalid_amount' })\n }\n const [source, invoice] = await Promise.all([\n loadSource(tem, scope, params.source),\n loadReceivable(tem, scope, params.invoiceId),\n ])\n if (!source) throw new CrudHttpError(404, { error: 'Payment source not found', code: 'billing.allocation.source_missing' })\n if (!invoice) throw new CrudHttpError(404, { error: 'Invoice not found', code: 'billing.allocation.invoice_missing' })\n if (!source.bill_account_id || source.bill_account_id !== invoice.bill_account_id) {\n throw new CrudHttpError(409, {\n error: 'Payment and invoice belong to different accounts',\n code: 'billing.allocation.account_mismatch',\n })\n }\n if (source.currency_code !== invoice.currency_code) {\n throw new CrudHttpError(409, { error: 'Currencies differ', code: 'billing.allocation.currency_mismatch' })\n }\n if (\n invoice.test_run ||\n !RECEIVABLE_TYPES.includes(invoice.document_type) ||\n !OPEN_STATUSES.includes(invoice.status)\n ) {\n throw new CrudHttpError(409, { error: 'Invoice is not open for payment', code: 'billing.allocation.invoice_not_open' })\n }\n if (params.amountCents > toCents(invoice.outstanding)) {\n throw new CrudHttpError(409, {\n error: 'Allocation exceeds the invoice outstanding amount',\n code: 'billing.allocation.exceeds_outstanding',\n })\n }\n const decremented = await adjustSourceUnallocated(tem, scope, params.source, params.amountCents)\n if (!decremented) {\n throw new CrudHttpError(409, {\n error: 'Not enough unallocated amount',\n code: 'billing.allocation.insufficient_unallocated',\n })\n }\n const now = new Date()\n const row = tem.create(BillingAllocation, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n sourceType: params.source.type,\n sourceId: params.source.id,\n invoiceId: params.invoiceId,\n billAccountId: invoice.bill_account_id,\n amount: fromCents(params.amountCents),\n currencyCode: invoice.currency_code,\n allocatedBy: params.by,\n userId: params.userId ?? null,\n createdAt: now,\n updatedAt: now,\n })\n tem.persist(row)\n await tem.flush()\n effects.created.push({\n allocationId: row.id,\n sourceType: params.source.type,\n sourceId: params.source.id,\n invoiceId: params.invoiceId,\n amount: row.amount,\n })\n const change = await recomputeInvoice(tem, scope, params.invoiceId)\n if (change) effects.invoiceStatusChanges.push(change)\n return effects\n}\n\nexport async function unallocate(\n tem: EntityManager,\n scope: ArScope,\n params: { allocationId: string; userId?: string | null },\n): Promise<AllocationEffects> {\n const effects = emptyEffects()\n const rows = (await tem.execute(\n `UPDATE billing_allocations\n SET deleted_at = now(), updated_at = now(), deallocated_by_user_id = ?\n WHERE id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL\n RETURNING id, source_type, source_id, invoice_id, amount`,\n [params.userId ?? null, params.allocationId, scope.tenantId, scope.organizationId],\n )) as unknown as Array<{ id: string; source_type: AllocationSourceType; source_id: string; invoice_id: string; amount: string }>\n const removed = rows?.[0]\n if (!removed) throw new CrudHttpError(404, { error: 'Allocation not found', code: 'billing.allocation.not_found' })\n await adjustSourceUnallocated(tem, scope, { type: removed.source_type, id: removed.source_id }, -toCents(removed.amount))\n effects.removed.push({\n allocationId: removed.id,\n sourceType: removed.source_type,\n sourceId: removed.source_id,\n invoiceId: removed.invoice_id,\n amount: removed.amount,\n })\n const change = await recomputeInvoice(tem, scope, removed.invoice_id)\n if (change) effects.invoiceStatusChanges.push(change)\n return effects\n}\n\n/** Open receivables of an account in auto-apply order. */\nexport async function loadOpenReceivables(\n tem: EntityManager,\n scope: ArScope,\n account: { id: string; currencyCode: string },\n onlyIds?: string[],\n): Promise<OpenReceivable[]> {\n const params: unknown[] = [scope.tenantId, scope.organizationId, account.id, account.currencyCode]\n let idFilter = ''\n if (onlyIds && onlyIds.length > 0) {\n idFilter = ` AND id = ANY(?::uuid[])`\n params.push(`{${onlyIds.join(',')}}`)\n }\n const rows = (await tem.execute(\n `SELECT id, outstanding, due_date, issue_date, number\n FROM billing_invoices\n WHERE tenant_id = ? AND organization_id = ? AND bill_account_id = ? AND currency_code = ?\n AND deleted_at IS NULL AND test_run = false\n AND document_type IN ('invoice', 'debit_memo')\n AND status IN ('posted', 'partially_paid')\n AND outstanding > 0${idFilter}`,\n params,\n )) as unknown as Array<{ id: string; outstanding: string; due_date: unknown; issue_date: unknown; number: string }>\n return sortOpenReceivables(\n (rows ?? []).map((row) => ({\n id: row.id,\n outstandingCents: toCents(row.outstanding),\n dueDate: isoDay(row.due_date),\n issueDate: isoDay(row.issue_date),\n number: row.number,\n })),\n )\n}\n\n/** Credit sources of an account (unallocated payments + open credit memos), oldest first. */\nexport async function loadCreditSources(\n tem: EntityManager,\n scope: ArScope,\n account: { id: string; currencyCode: string },\n): Promise<Array<{ source: AllocationSource; availableCents: number }>> {\n const rows = (await tem.execute(\n `SELECT 'payment' AS source_type, id, unallocated_amount AS available, received_at AS at, created_at\n FROM billing_payments\n WHERE tenant_id = ? AND organization_id = ? AND bill_account_id = ? AND currency_code = ?\n AND deleted_at IS NULL AND status IN ('matched', 'partially_allocated') AND unallocated_amount > 0\n UNION ALL\n SELECT 'credit_memo' AS source_type, id, outstanding AS available, issue_date AS at, created_at\n FROM billing_invoices\n WHERE tenant_id = ? AND organization_id = ? AND bill_account_id = ? AND currency_code = ?\n AND deleted_at IS NULL AND test_run = false AND document_type = 'credit_memo'\n AND status = 'posted' AND outstanding > 0\n ORDER BY at ASC, created_at ASC`,\n [\n scope.tenantId,\n scope.organizationId,\n account.id,\n account.currencyCode,\n scope.tenantId,\n scope.organizationId,\n account.id,\n account.currencyCode,\n ],\n )) as unknown as Array<{ source_type: AllocationSourceType; id: string; available: string }>\n return (rows ?? []).map((row) => ({\n source: { type: row.source_type, id: row.id },\n availableCents: toCents(row.available),\n }))\n}\n\n/**\n * Spends the account's credit on its open receivables (D12): sources oldest\n * first, receivables oldest due first. `onlySources` restricts the spend\n * (e.g. the payment just imported); default = all credit of the account.\n */\nexport async function applyAccountCredit(\n tem: EntityManager,\n scope: ArScope,\n account: { id: string; currencyCode: string },\n opts: { onlySources?: AllocationSource[]; by?: AllocatedBy } = {},\n): Promise<AllocationEffects> {\n const effects = emptyEffects()\n let sources = await loadCreditSources(tem, scope, account)\n if (opts.onlySources) {\n const wanted = new Set(opts.onlySources.map((s) => `${s.type}:${s.id}`))\n sources = sources.filter((s) => wanted.has(`${s.source.type}:${s.source.id}`))\n }\n if (sources.length === 0) return effects\n const receivables = await loadOpenReceivables(tem, scope, account)\n for (const credit of sources) {\n let available = credit.availableCents\n for (const receivable of receivables) {\n if (available <= 0) break\n const take = Math.min(available, receivable.outstandingCents)\n if (take <= 0) continue\n mergeEffects(\n effects,\n await allocate(tem, scope, {\n source: credit.source,\n invoiceId: receivable.id,\n amountCents: take,\n by: opts.by ?? 'credit',\n }),\n )\n receivable.outstandingCents -= take\n available -= take\n }\n }\n return effects\n}\n\n/** Unapplied credit of an account (\u03A3 unallocated payments + open credit memos). */\nexport async function getAccountCredit(\n em: EntityManager,\n scope: ArScope,\n account: { id: string; currencyCode: string },\n): Promise<string> {\n const sources = await loadCreditSources(em, scope, account)\n return fromCents(sources.reduce((sum, s) => sum + s.availableCents, 0))\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AA2B3B,SAAS,QAAQ,OAAmD;AACzE,QAAM,IAAI,OAAO,UAAU,WAAW,QAAQ,OAAO,WAAW,SAAS,GAAG;AAC5E,SAAO,OAAO,SAAS,CAAC,IAAI,KAAK,MAAM,IAAI,GAAG,IAAI;AACpD;AAEO,SAAS,UAAU,OAAuB;AAC/C,UAAQ,QAAQ,KAAK,QAAQ,CAAC;AAChC;AAWO,SAAS,oBAAwE,MAAgB;AACtG,SAAO,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,MAAM;AAC9B,QAAI,EAAE,YAAY,EAAE,SAAS;AAC3B,UAAI,CAAC,EAAE,QAAS,QAAO;AACvB,UAAI,CAAC,EAAE,QAAS,QAAO;AACvB,aAAO,EAAE,UAAU,EAAE,UAAU,KAAK;AAAA,IACtC;AACA,QAAI,EAAE,cAAc,EAAE,WAAW;AAC/B,UAAI,CAAC,EAAE,UAAW,QAAO;AACzB,UAAI,CAAC,EAAE,UAAW,QAAO;AACzB,aAAO,EAAE,YAAY,EAAE,YAAY,KAAK;AAAA,IAC1C;AACA,WAAO,EAAE,OAAO,cAAc,EAAE,MAAM;AAAA,EACxC,CAAC;AACH;AAGO,SAAS,eACd,aACA,aAC4F;AAC5F,MAAI,YAAY,KAAK,IAAI,GAAG,WAAW;AACvC,QAAM,cAAiE,CAAC;AACxE,aAAW,cAAc,aAAa;AACpC,QAAI,aAAa,EAAG;AACpB,UAAM,OAAO,KAAK,IAAI,WAAW,KAAK,IAAI,GAAG,WAAW,gBAAgB,CAAC;AACzE,QAAI,QAAQ,EAAG;AACf,gBAAY,KAAK,EAAE,WAAW,WAAW,IAAI,aAAa,KAAK,CAAC;AAChE,iBAAa;AAAA,EACf;AACA,SAAO,EAAE,aAAa,gBAAgB,UAAU;AAClD;AAUO,SAAS,oBAAoB,OAKlC;AACA,QAAM,EAAE,iBAAiB,OAAO,WAAW,MAAM,eAAe,SAAS,IAAI;AAC7E,QAAM,mBAAmB,KAAK,IAAI,GAAG,QAAQ,OAAO,QAAQ;AAC5D,MAAI,MAAM,WAAW,QAAS,QAAO,EAAE,WAAW,MAAM,eAAe,UAAU,kBAAkB,QAAQ,QAAQ;AACnH,MAAI;AACJ,MAAI,QAAQ,KAAK,qBAAqB,EAAG,UAAS,YAAY,QAAQ,aAAa;AAAA,WAC1E,OAAO,WAAW,EAAG,UAAS;AAAA,MAClC,UAAS;AACd,SAAO,EAAE,WAAW,MAAM,eAAe,UAAU,kBAAkB,OAAO;AAC9E;AAGO,SAAS,iBAAiB,SAKtB;AACT,MAAI,QAAQ,WAAW,aAAa,QAAQ,WAAW,qBAAsB,QAAO,QAAQ;AAC5F,MAAI,CAAC,QAAQ,cAAe,QAAO;AACnC,MAAI,QAAQ,oBAAoB,EAAG,QAAO;AAC1C,MAAI,QAAQ,mBAAmB,QAAQ,YAAa,QAAO;AAC3D,SAAO;AACT;AAYO,SAAS,eAAkC;AAChD,SAAO,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC,GAAG,sBAAsB,CAAC,EAAE;AAC9D;AAEO,SAAS,aAAa,QAA2B,OAA6C;AACnG,SAAO,QAAQ,KAAK,GAAG,MAAM,OAAO;AACpC,SAAO,QAAQ,KAAK,GAAG,MAAM,OAAO;AACpC,SAAO,qBAAqB,KAAK,GAAG,MAAM,oBAAoB;AAC9D,SAAO;AACT;AA8BA,MAAM,mBAAmB,CAAC,WAAW,YAAY;AACjD,MAAM,gBAAgB,CAAC,UAAU,gBAAgB;AAEjD,SAAS,OAAO,OAA+B;AAC7C,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY,EAAE,MAAM,GAAG,EAAE;AACjE,SAAO,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AAClC;AAEA,eAAe,eAAe,KAAoB,OAAgB,WAAkD;AAClH,QAAM,OAAQ,MAAM,IAAI;AAAA,IACtB;AAAA;AAAA;AAAA;AAAA,IAIA,CAAC,WAAW,MAAM,UAAU,MAAM,cAAc;AAAA,EAClD;AACA,SAAO,OAAO,CAAC,KAAK;AACtB;AAEA,eAAe,WAAW,KAAoB,OAAgB,QAAqD;AACjH,QAAM,MACJ,OAAO,SAAS,YACZ;AAAA;AAAA,2FAGA;AAAA;AAAA;AAAA;AAIN,QAAM,OAAQ,MAAM,IAAI,QAAQ,KAAK,CAAC,OAAO,IAAI,MAAM,UAAU,MAAM,cAAc,CAAC;AACtF,SAAO,OAAO,CAAC,KAAK;AACtB;AAGA,eAAe,wBACb,KACA,OACA,QACA,YACkB;AAClB,QAAM,SAAS,UAAU,KAAK,IAAI,UAAU,CAAC;AAC7C,MAAI,OAAO,SAAS,WAAW;AAC7B,UAAMA,QAAQ,MAAM,IAAI;AAAA,MACtB,cAAc,IACV;AAAA;AAAA;AAAA,0BAIA;AAAA;AAAA;AAAA;AAAA,MAIJ,cAAc,IACV,CAAC,QAAQ,OAAO,IAAI,MAAM,UAAU,MAAM,gBAAgB,MAAM,IAChE,CAAC,QAAQ,OAAO,IAAI,MAAM,UAAU,MAAM,cAAc;AAAA,IAC9D;AACA,QAAI,CAACA,OAAM,OAAQ,QAAO;AAC1B,UAAM,IAAI;AAAA,MACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,CAAC,OAAO,EAAE;AAAA,IACZ;AACA,WAAO;AAAA,EACT;AAEA,QAAM,OAAQ,MAAM,IAAI;AAAA,IACtB,cAAc,IACV;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAOA;AAAA;AAAA;AAAA;AAAA,IAIJ,cAAc,IACV,CAAC,QAAQ,QAAQ,OAAO,IAAI,MAAM,UAAU,MAAM,gBAAgB,MAAM,IACxE,CAAC,QAAQ,OAAO,IAAI,MAAM,UAAU,MAAM,cAAc;AAAA,EAC9D;AACA,SAAO,QAAQ,MAAM,MAAM;AAC7B;AAGA,eAAsB,iBACpB,KACA,OACA,WACqC;AACrC,QAAM,UAAU,MAAM,eAAe,KAAK,OAAO,SAAS;AAC1D,MAAI,CAAC,WAAW,CAAC,iBAAiB,SAAS,QAAQ,aAAa,EAAG,QAAO;AAC1E,QAAM,OAAQ,MAAM,IAAI;AAAA,IACtB;AAAA;AAAA;AAAA;AAAA,IAIA,CAAC,WAAW,MAAM,UAAU,MAAM,cAAc;AAAA,EAClD;AACA,QAAM,OAAO,oBAAoB;AAAA,IAC/B,QAAQ,QAAQ;AAAA,IAChB,iBAAiB,QAAQ,QAAQ,WAAW;AAAA,IAC5C,WAAW,QAAQ,OAAO,CAAC,GAAG,IAAI;AAAA,IAClC,eAAe,QAAQ,OAAO,CAAC,GAAG,QAAQ;AAAA,EAC5C,CAAC;AACD,QAAM,IAAI;AAAA,IACR;AAAA;AAAA;AAAA,IAGA;AAAA,MACE,UAAU,KAAK,SAAS;AAAA,MACxB,UAAU,KAAK,aAAa;AAAA,MAC5B,UAAU,KAAK,gBAAgB;AAAA,MAC/B,KAAK;AAAA,MACL;AAAA,MACA,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AAAA,EACF;AACA,SAAO,KAAK,WAAW,QAAQ,SAAS,EAAE,WAAW,MAAM,QAAQ,QAAQ,IAAI,KAAK,OAAO,IAAI;AACjG;AAEA,eAAsB,SACpB,KACA,OACA,QAC4B;AAC5B,QAAM,UAAU,aAAa;AAC7B,MAAI,OAAO,eAAe,GAAG;AAC3B,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,sCAAsC,MAAM,oCAAoC,CAAC;AAAA,EACzH;AACA,QAAM,CAAC,QAAQ,OAAO,IAAI,MAAM,QAAQ,IAAI;AAAA,IAC1C,WAAW,KAAK,OAAO,OAAO,MAAM;AAAA,IACpC,eAAe,KAAK,OAAO,OAAO,SAAS;AAAA,EAC7C,CAAC;AACD,MAAI,CAAC,OAAQ,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,4BAA4B,MAAM,oCAAoC,CAAC;AAC1H,MAAI,CAAC,QAAS,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,qBAAqB,MAAM,qCAAqC,CAAC;AACrH,MAAI,CAAC,OAAO,mBAAmB,OAAO,oBAAoB,QAAQ,iBAAiB;AACjF,UAAM,IAAI,cAAc,KAAK;AAAA,MAC3B,OAAO;AAAA,MACP,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACA,MAAI,OAAO,kBAAkB,QAAQ,eAAe;AAClD,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,qBAAqB,MAAM,uCAAuC,CAAC;AAAA,EAC3G;AACA,MACE,QAAQ,YACR,CAAC,iBAAiB,SAAS,QAAQ,aAAa,KAChD,CAAC,cAAc,SAAS,QAAQ,MAAM,GACtC;AACA,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,mCAAmC,MAAM,sCAAsC,CAAC;AAAA,EACxH;AACA,MAAI,OAAO,cAAc,QAAQ,QAAQ,WAAW,GAAG;AACrD,UAAM,IAAI,cAAc,KAAK;AAAA,MAC3B,OAAO;AAAA,MACP,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACA,QAAM,cAAc,MAAM,wBAAwB,KAAK,OAAO,OAAO,QAAQ,OAAO,WAAW;AAC/F,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,cAAc,KAAK;AAAA,MAC3B,OAAO;AAAA,MACP,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACA,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,MAAM,IAAI,OAAO,mBAAmB;AAAA,IACxC,gBAAgB,MAAM;AAAA,IACtB,UAAU,MAAM;AAAA,IAChB,YAAY,OAAO,OAAO;AAAA,IAC1B,UAAU,OAAO,OAAO;AAAA,IACxB,WAAW,OAAO;AAAA,IAClB,eAAe,QAAQ;AAAA,IACvB,QAAQ,UAAU,OAAO,WAAW;AAAA,IACpC,cAAc,QAAQ;AAAA,IACtB,aAAa,OAAO;AAAA,IACpB,QAAQ,OAAO,UAAU;AAAA,IACzB,WAAW;AAAA,IACX,WAAW;AAAA,EACb,CAAC;AACD,MAAI,QAAQ,GAAG;AACf,QAAM,IAAI,MAAM;AAChB,UAAQ,QAAQ,KAAK;AAAA,IACnB,cAAc,IAAI;AAAA,IAClB,YAAY,OAAO,OAAO;AAAA,IAC1B,UAAU,OAAO,OAAO;AAAA,IACxB,WAAW,OAAO;AAAA,IAClB,QAAQ,IAAI;AAAA,EACd,CAAC;AACD,QAAM,SAAS,MAAM,iBAAiB,KAAK,OAAO,OAAO,SAAS;AAClE,MAAI,OAAQ,SAAQ,qBAAqB,KAAK,MAAM;AACpD,SAAO;AACT;AAEA,eAAsB,WACpB,KACA,OACA,QAC4B;AAC5B,QAAM,UAAU,aAAa;AAC7B,QAAM,OAAQ,MAAM,IAAI;AAAA,IACtB;AAAA;AAAA;AAAA;AAAA,IAIA,CAAC,OAAO,UAAU,MAAM,OAAO,cAAc,MAAM,UAAU,MAAM,cAAc;AAAA,EACnF;AACA,QAAM,UAAU,OAAO,CAAC;AACxB,MAAI,CAAC,QAAS,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,wBAAwB,MAAM,+BAA+B,CAAC;AAClH,QAAM,wBAAwB,KAAK,OAAO,EAAE,MAAM,QAAQ,aAAa,IAAI,QAAQ,UAAU,GAAG,CAAC,QAAQ,QAAQ,MAAM,CAAC;AACxH,UAAQ,QAAQ,KAAK;AAAA,IACnB,cAAc,QAAQ;AAAA,IACtB,YAAY,QAAQ;AAAA,IACpB,UAAU,QAAQ;AAAA,IAClB,WAAW,QAAQ;AAAA,IACnB,QAAQ,QAAQ;AAAA,EAClB,CAAC;AACD,QAAM,SAAS,MAAM,iBAAiB,KAAK,OAAO,QAAQ,UAAU;AACpE,MAAI,OAAQ,SAAQ,qBAAqB,KAAK,MAAM;AACpD,SAAO;AACT;AAGA,eAAsB,oBACpB,KACA,OACA,SACA,SAC2B;AAC3B,QAAM,SAAoB,CAAC,MAAM,UAAU,MAAM,gBAAgB,QAAQ,IAAI,QAAQ,YAAY;AACjG,MAAI,WAAW;AACf,MAAI,WAAW,QAAQ,SAAS,GAAG;AACjC,eAAW;AACX,WAAO,KAAK,IAAI,QAAQ,KAAK,GAAG,CAAC,GAAG;AAAA,EACtC;AACA,QAAM,OAAQ,MAAM,IAAI;AAAA,IACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAMyB,QAAQ;AAAA,IACjC;AAAA,EACF;AACA,SAAO;AAAA,KACJ,QAAQ,CAAC,GAAG,IAAI,CAAC,SAAS;AAAA,MACzB,IAAI,IAAI;AAAA,MACR,kBAAkB,QAAQ,IAAI,WAAW;AAAA,MACzC,SAAS,OAAO,IAAI,QAAQ;AAAA,MAC5B,WAAW,OAAO,IAAI,UAAU;AAAA,MAChC,QAAQ,IAAI;AAAA,IACd,EAAE;AAAA,EACJ;AACF;AAGA,eAAsB,kBACpB,KACA,OACA,SACsE;AACtE,QAAM,OAAQ,MAAM,IAAI;AAAA,IACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAWA;AAAA,MACE,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,EACF;AACA,UAAQ,QAAQ,CAAC,GAAG,IAAI,CAAC,SAAS;AAAA,IAChC,QAAQ,EAAE,MAAM,IAAI,aAAa,IAAI,IAAI,GAAG;AAAA,IAC5C,gBAAgB,QAAQ,IAAI,SAAS;AAAA,EACvC,EAAE;AACJ;AAOA,eAAsB,mBACpB,KACA,OACA,SACA,OAA+D,CAAC,GACpC;AAC5B,QAAM,UAAU,aAAa;AAC7B,MAAI,UAAU,MAAM,kBAAkB,KAAK,OAAO,OAAO;AACzD,MAAI,KAAK,aAAa;AACpB,UAAM,SAAS,IAAI,IAAI,KAAK,YAAY,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,IAAI,EAAE,EAAE,EAAE,CAAC;AACvE,cAAU,QAAQ,OAAO,CAAC,MAAM,OAAO,IAAI,GAAG,EAAE,OAAO,IAAI,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC;AAAA,EAC/E;AACA,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAM,cAAc,MAAM,oBAAoB,KAAK,OAAO,OAAO;AACjE,aAAW,UAAU,SAAS;AAC5B,QAAI,YAAY,OAAO;AACvB,eAAW,cAAc,aAAa;AACpC,UAAI,aAAa,EAAG;AACpB,YAAM,OAAO,KAAK,IAAI,WAAW,WAAW,gBAAgB;AAC5D,UAAI,QAAQ,EAAG;AACf;AAAA,QACE;AAAA,QACA,MAAM,SAAS,KAAK,OAAO;AAAA,UACzB,QAAQ,OAAO;AAAA,UACf,WAAW,WAAW;AAAA,UACtB,aAAa;AAAA,UACb,IAAI,KAAK,MAAM;AAAA,QACjB,CAAC;AAAA,MACH;AACA,iBAAW,oBAAoB;AAC/B,mBAAa;AAAA,IACf;AAAA,EACF;AACA,SAAO;AACT;AAGA,eAAsB,iBACpB,IACA,OACA,SACiB;AACjB,QAAM,UAAU,MAAM,kBAAkB,IAAI,OAAO,OAAO;AAC1D,SAAO,UAAU,QAAQ,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,gBAAgB,CAAC,CAAC;AACxE;",
|
|
6
|
+
"names": ["rows"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { emitBillingEvent } from "../events.js";
|
|
2
|
+
async function emitAllocationEffects(scope, effects) {
|
|
3
|
+
for (const row of effects.created) {
|
|
4
|
+
if (row.sourceType !== "payment") continue;
|
|
5
|
+
await emitBillingEvent("billing.payment.allocated", { ...scope, paymentId: row.sourceId, invoiceId: row.invoiceId, allocationId: row.allocationId, amount: row.amount });
|
|
6
|
+
}
|
|
7
|
+
for (const row of effects.removed) {
|
|
8
|
+
if (row.sourceType !== "payment") continue;
|
|
9
|
+
await emitBillingEvent("billing.payment.unallocated", { ...scope, paymentId: row.sourceId, invoiceId: row.invoiceId, allocationId: row.allocationId, amount: row.amount });
|
|
10
|
+
}
|
|
11
|
+
const finalStatus = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const change of effects.invoiceStatusChanges) finalStatus.set(change.invoiceId, change.to);
|
|
13
|
+
for (const [invoiceId, status] of finalStatus) {
|
|
14
|
+
if (status === "paid") await emitBillingEvent("billing.invoice.paid", { ...scope, invoiceId });
|
|
15
|
+
else if (status === "partially_paid") await emitBillingEvent("billing.invoice.partially_paid", { ...scope, invoiceId });
|
|
16
|
+
else if (status === "credited") await emitBillingEvent("billing.invoice.credited", { ...scope, invoiceId });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
emitAllocationEffects
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=allocationEvents.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/allocationEvents.ts"],
|
|
4
|
+
"sourcesContent": ["import { emitBillingEvent } from '../events'\nimport type { AllocationEffects, ArScope } from './allocation'\n\n/**\n * Emits the events of an allocation change AFTER the transaction committed\n * (Spec C): one `billing.payment.allocated` / `unallocated` per payment\n * allocation row, and `billing.invoice.paid` / `partially_paid` / `credited` (Spec D) per receivable whose status changed.\n */\nexport async function emitAllocationEffects(scope: ArScope, effects: AllocationEffects): Promise<void> {\n for (const row of effects.created) {\n if (row.sourceType !== 'payment') continue\n await emitBillingEvent('billing.payment.allocated', { ...scope, paymentId: row.sourceId, invoiceId: row.invoiceId, allocationId: row.allocationId, amount: row.amount })\n }\n for (const row of effects.removed) {\n if (row.sourceType !== 'payment') continue\n await emitBillingEvent('billing.payment.unallocated', { ...scope, paymentId: row.sourceId, invoiceId: row.invoiceId, allocationId: row.allocationId, amount: row.amount })\n }\n // Last transition per invoice wins (several allocations can touch one invoice).\n const finalStatus = new Map<string, string>()\n for (const change of effects.invoiceStatusChanges) finalStatus.set(change.invoiceId, change.to)\n for (const [invoiceId, status] of finalStatus) {\n if (status === 'paid') await emitBillingEvent('billing.invoice.paid', { ...scope, invoiceId })\n else if (status === 'partially_paid') await emitBillingEvent('billing.invoice.partially_paid', { ...scope, invoiceId })\n else if (status === 'credited') await emitBillingEvent('billing.invoice.credited', { ...scope, invoiceId })\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,wBAAwB;AAQjC,eAAsB,sBAAsB,OAAgB,SAA2C;AACrG,aAAW,OAAO,QAAQ,SAAS;AACjC,QAAI,IAAI,eAAe,UAAW;AAClC,UAAM,iBAAiB,6BAA6B,EAAE,GAAG,OAAO,WAAW,IAAI,UAAU,WAAW,IAAI,WAAW,cAAc,IAAI,cAAc,QAAQ,IAAI,OAAO,CAAC;AAAA,EACzK;AACA,aAAW,OAAO,QAAQ,SAAS;AACjC,QAAI,IAAI,eAAe,UAAW;AAClC,UAAM,iBAAiB,+BAA+B,EAAE,GAAG,OAAO,WAAW,IAAI,UAAU,WAAW,IAAI,WAAW,cAAc,IAAI,cAAc,QAAQ,IAAI,OAAO,CAAC;AAAA,EAC3K;AAEA,QAAM,cAAc,oBAAI,IAAoB;AAC5C,aAAW,UAAU,QAAQ,qBAAsB,aAAY,IAAI,OAAO,WAAW,OAAO,EAAE;AAC9F,aAAW,CAAC,WAAW,MAAM,KAAK,aAAa;AAC7C,QAAI,WAAW,OAAQ,OAAM,iBAAiB,wBAAwB,EAAE,GAAG,OAAO,UAAU,CAAC;AAAA,aACpF,WAAW,iBAAkB,OAAM,iBAAiB,kCAAkC,EAAE,GAAG,OAAO,UAAU,CAAC;AAAA,aAC7G,WAAW,WAAY,OAAM,iBAAiB,4BAA4B,EAAE,GAAG,OAAO,UAAU,CAAC;AAAA,EAC5G;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -90,12 +90,26 @@ function isCycleDue(nextBillDate, asOfDate) {
|
|
|
90
90
|
const b = cloneAtMidnight(asOfDate).getTime();
|
|
91
91
|
return a <= b;
|
|
92
92
|
}
|
|
93
|
+
function shiftCycles(anchor, cycle, k) {
|
|
94
|
+
const base = cloneAtMidnight(anchor);
|
|
95
|
+
switch (cycle) {
|
|
96
|
+
case "weekly":
|
|
97
|
+
return addDays(base, 7 * k);
|
|
98
|
+
case "monthly":
|
|
99
|
+
return shiftMonths(base, k);
|
|
100
|
+
case "quarterly":
|
|
101
|
+
return shiftMonths(base, 3 * k);
|
|
102
|
+
case "annually":
|
|
103
|
+
return shiftYears(base, k);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
93
106
|
export {
|
|
94
107
|
addDays,
|
|
95
108
|
advanceNextBillDate,
|
|
96
109
|
cloneAtMidnight,
|
|
97
110
|
cycleContaining,
|
|
98
111
|
deriveBillPeriod,
|
|
99
|
-
isCycleDue
|
|
112
|
+
isCycleDue,
|
|
113
|
+
shiftCycles
|
|
100
114
|
};
|
|
101
115
|
//# sourceMappingURL=billPeriod.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/lib/billPeriod.ts"],
|
|
4
|
-
"sourcesContent": ["import type { BillCycle } from '../data/validators'\n\n/**\n * Calendar-aware bill-period arithmetic.\n *\n * The spec mandates calendar units (Postgres `interval`-equivalent),\n * NOT raw day counts \u2014 months and quarters have variable length, so a\n * 30-day subtraction would skew the period boundary every February.\n *\n * All helpers here treat input dates as midnight in the host timezone\n * (the way MikroORM's `date`-column round-trip lands them). Date math\n * via `Date.setMonth` / `setDate` is the right primitive: Postgres'\n * `+ interval '1 month'` and JS's `setMonth(+1)` agree on the\n * \"clamp to month end\" behaviour (e.g. Jan 31 + 1 month \u2192 Feb 28/29).\n *\n * Examples (all from the spec's reference table):\n *\n * monthly next=2026-06-01 \u2192 period=2026-05-01..2026-05-31\n * monthly next=2026-03-01 \u2192 period=2026-02-01..2026-02-28\n * monthly next=2024-03-01 \u2192 period=2024-02-01..2024-02-29 (leap)\n * quarterly next=2026-07-01 \u2192 period=2026-04-01..2026-06-30\n * weekly next=2026-05-04 (Mon) \u2192 period=2026-04-27..2026-05-03\n */\n\nexport type BillPeriod = {\n /** Inclusive \u2014 first day of the cycle being billed. */\n periodStart: Date\n /** Inclusive \u2014 last day of the cycle (`nextBillDate - 1 day`). */\n periodEnd: Date\n}\n\nfunction cloneAtMidnight(date: Date | string | number): Date {\n // MikroORM hands `date`-typed columns back as 'YYYY-MM-DD' strings,\n // not Date objects \u2014 coerce so every helper here is input-tolerant\n // whether called from the engine (DB rows) or unit tests (plain\n // `new Date('2026-06-01')` values).\n const result = new Date(date instanceof Date ? date.getTime() : date)\n result.setUTCHours(0, 0, 0, 0)\n return result\n}\n\nfunction subtractDays(date: Date, days: number): Date {\n const result = cloneAtMidnight(date)\n result.setUTCDate(result.getUTCDate() - days)\n return result\n}\n\nfunction addDays(date: Date, days: number): Date {\n const result = cloneAtMidnight(date)\n result.setUTCDate(result.getUTCDate() + days)\n return result\n}\n\nfunction lastDayOfMonthUTC(year: number, monthZeroIndexed: number): number {\n // Date(Date.UTC(y, m+1, 0)) returns the last day of month m. The\n // zero-day-of-next-month trick is the canonical JS idiom.\n return new Date(Date.UTC(year, monthZeroIndexed + 1, 0)).getUTCDate()\n}\n\nfunction shiftMonths(date: Date, deltaMonths: number): Date {\n // JS `setUTCMonth` does NOT clamp when the target month is shorter\n // than the source day \u2014 it overflows (Jan 31 + 1 month \u2192 Mar 3\n // because Feb 31 spills over). Postgres `+ interval '1 month'` and\n // the spec both clamp, so we replicate that: shift the month while\n // pinned to day 1, then restore the original day clamped to the new\n // month's last day.\n const result = cloneAtMidnight(date)\n const day = result.getUTCDate()\n result.setUTCDate(1)\n result.setUTCMonth(result.getUTCMonth() + deltaMonths)\n const lastDay = lastDayOfMonthUTC(result.getUTCFullYear(), result.getUTCMonth())\n result.setUTCDate(Math.min(day, lastDay))\n return result\n}\n\nfunction shiftYears(date: Date, deltaYears: number): Date {\n // Same clamp story for Feb 29 in non-leap years.\n const result = cloneAtMidnight(date)\n const day = result.getUTCDate()\n result.setUTCDate(1)\n result.setUTCFullYear(result.getUTCFullYear() + deltaYears)\n const lastDay = lastDayOfMonthUTC(result.getUTCFullYear(), result.getUTCMonth())\n result.setUTCDate(Math.min(day, lastDay))\n return result\n}\n\nfunction subtractCycle(date: Date, cycle: BillCycle): Date {\n switch (cycle) {\n case 'weekly': {\n const result = cloneAtMidnight(date)\n result.setUTCDate(result.getUTCDate() - 7)\n return result\n }\n case 'monthly':\n return shiftMonths(date, -1)\n case 'quarterly':\n return shiftMonths(date, -3)\n case 'annually':\n return shiftYears(date, -1)\n }\n}\n\nfunction addCycle(date: Date, cycle: BillCycle): Date {\n switch (cycle) {\n case 'weekly': {\n const result = cloneAtMidnight(date)\n result.setUTCDate(result.getUTCDate() + 7)\n return result\n }\n case 'monthly':\n return shiftMonths(date, 1)\n case 'quarterly':\n return shiftMonths(date, 3)\n case 'annually':\n return shiftYears(date, 1)\n }\n}\n\n/**\n * Compute the bill period for the cycle that `nextBillDate` marks the\n * START of the *next* one. So `periodEnd = nextBillDate - 1 day` and\n * `periodStart = nextBillDate - cycle`.\n */\nexport function deriveBillPeriod(nextBillDate: Date, cycle: BillCycle): BillPeriod {\n const periodEnd = subtractDays(nextBillDate, 1)\n const periodStart = subtractCycle(nextBillDate, cycle)\n return { periodStart, periodEnd }\n}\n\n/**\n * Advance `nextBillDate` by one cycle. Used by the engine after a\n * successful real-mode account run.\n */\nexport function advanceNextBillDate(currentNextBillDate: Date, cycle: BillCycle): Date {\n return addCycle(currentNextBillDate, cycle)\n}\n\n/**\n * The bill cycle that `target` falls into, given any known cycle\n * boundary `anchor` (e.g. an account's `nextBillDate`). Returned\n * `periodStart` is inclusive, `periodEnd` is the last day of the cycle.\n *\n * Used by the cpq-billing-connector to prorate a mid-cycle amendment \u2014\n * the connector owns the billing-cycle knowledge, CPQ does not.\n */\nexport function cycleContaining(\n anchor: Date | string | number,\n cycle: BillCycle,\n target: Date | string | number,\n): BillPeriod {\n const targetMs = cloneAtMidnight(target).getTime()\n let start = cloneAtMidnight(anchor)\n let guard = 0\n // Walk forward while the cycle starting at `start` ends before target.\n while (addCycle(start, cycle).getTime() <= targetMs && guard++ < 600) {\n start = addCycle(start, cycle)\n }\n guard = 0\n // Walk backward while `start` itself is after target.\n while (start.getTime() > targetMs && guard++ < 600) {\n start = subtractCycle(start, cycle)\n }\n return { periodStart: start, periodEnd: subtractDays(addCycle(start, cycle), 1) }\n}\n\n/**\n * True when the account still has cycles to bill on or before\n * `asOfDate`. The engine uses this to drive the catch-up loop.\n */\nexport function isCycleDue(nextBillDate: Date, asOfDate: Date): boolean {\n const a = cloneAtMidnight(nextBillDate).getTime()\n const b = cloneAtMidnight(asOfDate).getTime()\n return a <= b\n}\n\n/**\n * Test seam: re-export the day-add helper so the engine can compute\n * `bill_period_end + 1 day` consistently with this module's calendar\n * semantics (avoids drift between engine code and period-math tests).\n */\nexport { addDays, cloneAtMidnight }\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { BillCycle } from '../data/validators'\n\n/**\n * Calendar-aware bill-period arithmetic.\n *\n * The spec mandates calendar units (Postgres `interval`-equivalent),\n * NOT raw day counts \u2014 months and quarters have variable length, so a\n * 30-day subtraction would skew the period boundary every February.\n *\n * All helpers here treat input dates as midnight in the host timezone\n * (the way MikroORM's `date`-column round-trip lands them). Date math\n * via `Date.setMonth` / `setDate` is the right primitive: Postgres'\n * `+ interval '1 month'` and JS's `setMonth(+1)` agree on the\n * \"clamp to month end\" behaviour (e.g. Jan 31 + 1 month \u2192 Feb 28/29).\n *\n * Examples (all from the spec's reference table):\n *\n * monthly next=2026-06-01 \u2192 period=2026-05-01..2026-05-31\n * monthly next=2026-03-01 \u2192 period=2026-02-01..2026-02-28\n * monthly next=2024-03-01 \u2192 period=2024-02-01..2024-02-29 (leap)\n * quarterly next=2026-07-01 \u2192 period=2026-04-01..2026-06-30\n * weekly next=2026-05-04 (Mon) \u2192 period=2026-04-27..2026-05-03\n */\n\nexport type BillPeriod = {\n /** Inclusive \u2014 first day of the cycle being billed. */\n periodStart: Date\n /** Inclusive \u2014 last day of the cycle (`nextBillDate - 1 day`). */\n periodEnd: Date\n}\n\nfunction cloneAtMidnight(date: Date | string | number): Date {\n // MikroORM hands `date`-typed columns back as 'YYYY-MM-DD' strings,\n // not Date objects \u2014 coerce so every helper here is input-tolerant\n // whether called from the engine (DB rows) or unit tests (plain\n // `new Date('2026-06-01')` values).\n const result = new Date(date instanceof Date ? date.getTime() : date)\n result.setUTCHours(0, 0, 0, 0)\n return result\n}\n\nfunction subtractDays(date: Date, days: number): Date {\n const result = cloneAtMidnight(date)\n result.setUTCDate(result.getUTCDate() - days)\n return result\n}\n\nfunction addDays(date: Date, days: number): Date {\n const result = cloneAtMidnight(date)\n result.setUTCDate(result.getUTCDate() + days)\n return result\n}\n\nfunction lastDayOfMonthUTC(year: number, monthZeroIndexed: number): number {\n // Date(Date.UTC(y, m+1, 0)) returns the last day of month m. The\n // zero-day-of-next-month trick is the canonical JS idiom.\n return new Date(Date.UTC(year, monthZeroIndexed + 1, 0)).getUTCDate()\n}\n\nfunction shiftMonths(date: Date, deltaMonths: number): Date {\n // JS `setUTCMonth` does NOT clamp when the target month is shorter\n // than the source day \u2014 it overflows (Jan 31 + 1 month \u2192 Mar 3\n // because Feb 31 spills over). Postgres `+ interval '1 month'` and\n // the spec both clamp, so we replicate that: shift the month while\n // pinned to day 1, then restore the original day clamped to the new\n // month's last day.\n const result = cloneAtMidnight(date)\n const day = result.getUTCDate()\n result.setUTCDate(1)\n result.setUTCMonth(result.getUTCMonth() + deltaMonths)\n const lastDay = lastDayOfMonthUTC(result.getUTCFullYear(), result.getUTCMonth())\n result.setUTCDate(Math.min(day, lastDay))\n return result\n}\n\nfunction shiftYears(date: Date, deltaYears: number): Date {\n // Same clamp story for Feb 29 in non-leap years.\n const result = cloneAtMidnight(date)\n const day = result.getUTCDate()\n result.setUTCDate(1)\n result.setUTCFullYear(result.getUTCFullYear() + deltaYears)\n const lastDay = lastDayOfMonthUTC(result.getUTCFullYear(), result.getUTCMonth())\n result.setUTCDate(Math.min(day, lastDay))\n return result\n}\n\nfunction subtractCycle(date: Date, cycle: BillCycle): Date {\n switch (cycle) {\n case 'weekly': {\n const result = cloneAtMidnight(date)\n result.setUTCDate(result.getUTCDate() - 7)\n return result\n }\n case 'monthly':\n return shiftMonths(date, -1)\n case 'quarterly':\n return shiftMonths(date, -3)\n case 'annually':\n return shiftYears(date, -1)\n }\n}\n\nfunction addCycle(date: Date, cycle: BillCycle): Date {\n switch (cycle) {\n case 'weekly': {\n const result = cloneAtMidnight(date)\n result.setUTCDate(result.getUTCDate() + 7)\n return result\n }\n case 'monthly':\n return shiftMonths(date, 1)\n case 'quarterly':\n return shiftMonths(date, 3)\n case 'annually':\n return shiftYears(date, 1)\n }\n}\n\n/**\n * Compute the bill period for the cycle that `nextBillDate` marks the\n * START of the *next* one. So `periodEnd = nextBillDate - 1 day` and\n * `periodStart = nextBillDate - cycle`.\n */\nexport function deriveBillPeriod(nextBillDate: Date, cycle: BillCycle): BillPeriod {\n const periodEnd = subtractDays(nextBillDate, 1)\n const periodStart = subtractCycle(nextBillDate, cycle)\n return { periodStart, periodEnd }\n}\n\n/**\n * Advance `nextBillDate` by one cycle. Used by the engine after a\n * successful real-mode account run.\n */\nexport function advanceNextBillDate(currentNextBillDate: Date, cycle: BillCycle): Date {\n return addCycle(currentNextBillDate, cycle)\n}\n\n/**\n * The bill cycle that `target` falls into, given any known cycle\n * boundary `anchor` (e.g. an account's `nextBillDate`). Returned\n * `periodStart` is inclusive, `periodEnd` is the last day of the cycle.\n *\n * Used by the cpq-billing-connector to prorate a mid-cycle amendment \u2014\n * the connector owns the billing-cycle knowledge, CPQ does not.\n */\nexport function cycleContaining(\n anchor: Date | string | number,\n cycle: BillCycle,\n target: Date | string | number,\n): BillPeriod {\n const targetMs = cloneAtMidnight(target).getTime()\n let start = cloneAtMidnight(anchor)\n let guard = 0\n // Walk forward while the cycle starting at `start` ends before target.\n while (addCycle(start, cycle).getTime() <= targetMs && guard++ < 600) {\n start = addCycle(start, cycle)\n }\n guard = 0\n // Walk backward while `start` itself is after target.\n while (start.getTime() > targetMs && guard++ < 600) {\n start = subtractCycle(start, cycle)\n }\n return { periodStart: start, periodEnd: subtractDays(addCycle(start, cycle), 1) }\n}\n\n/**\n * True when the account still has cycles to bill on or before\n * `asOfDate`. The engine uses this to drive the catch-up loop.\n */\nexport function isCycleDue(nextBillDate: Date, asOfDate: Date): boolean {\n const a = cloneAtMidnight(nextBillDate).getTime()\n const b = cloneAtMidnight(asOfDate).getTime()\n return a <= b\n}\n\n/**\n * The cycle boundary `k` cycles away from `anchor` (k may be negative),\n * computed directly from the anchor \u2014 never by chaining single steps \u2014 so\n * month-end clamping does not drift (Jan 31 \u2192 Feb 28 \u2192 Mar 31, not Mar 28).\n */\nexport function shiftCycles(anchor: Date | string | number, cycle: BillCycle, k: number): Date {\n const base = cloneAtMidnight(anchor)\n switch (cycle) {\n case 'weekly':\n return addDays(base, 7 * k)\n case 'monthly':\n return shiftMonths(base, k)\n case 'quarterly':\n return shiftMonths(base, 3 * k)\n case 'annually':\n return shiftYears(base, k)\n }\n}\n\n/**\n * Test seam: re-export the day-add helper so the engine can compute\n * `bill_period_end + 1 day` consistently with this module's calendar\n * semantics (avoids drift between engine code and period-math tests).\n */\nexport { addDays, cloneAtMidnight }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|