@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,131 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
3
|
+
import { BillingAccount, BillingInvoice } from "../data/entities.js";
|
|
4
|
+
import { emitBillingEvent } from "../events.js";
|
|
5
|
+
import { DEFAULT_INVOICE_LANGUAGE } from "./invoiceLanguages.js";
|
|
6
|
+
import { formatIban } from "./iban.js";
|
|
7
|
+
import { documentLocale, formatDocumentDate, formatDocumentMoney } from "./pdf/invoiceModel.js";
|
|
8
|
+
import { getInvoicePdf, loadDocumentTranslator } from "./pdf/invoicePdf.js";
|
|
9
|
+
const PLACEHOLDER_DOMAIN = "@invalid.local";
|
|
10
|
+
const emailSchema = z.string().trim().email();
|
|
11
|
+
function isDeliverableEmail(value) {
|
|
12
|
+
if (!value) return false;
|
|
13
|
+
if (value.trim().toLowerCase().endsWith(PLACEHOLDER_DOMAIN)) return false;
|
|
14
|
+
return emailSchema.safeParse(value).success;
|
|
15
|
+
}
|
|
16
|
+
function hasSuccessfulDelivery(invoice, trigger) {
|
|
17
|
+
const deliveries = invoice.metadata?.deliveries ?? [];
|
|
18
|
+
return Array.isArray(deliveries) && deliveries.some((d) => d.status === "sent" && (!trigger || d.trigger === trigger));
|
|
19
|
+
}
|
|
20
|
+
function escapeHtml(value) {
|
|
21
|
+
return value.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] ?? c);
|
|
22
|
+
}
|
|
23
|
+
function fill(template, params) {
|
|
24
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => params[key] ?? match);
|
|
25
|
+
}
|
|
26
|
+
function buildInvoiceEmail(params) {
|
|
27
|
+
const { invoice, translate: t } = params;
|
|
28
|
+
const locale = documentLocale(params.language);
|
|
29
|
+
const values = {
|
|
30
|
+
number: invoice.number,
|
|
31
|
+
name: params.buyerName,
|
|
32
|
+
amount: formatDocumentMoney(invoice.totalGross, invoice.currencyCode, locale).replace(/[ ]/g, " "),
|
|
33
|
+
dueDate: formatDocumentDate(invoice.dueDate ?? null, locale),
|
|
34
|
+
iban: params.iban ? formatIban(params.iban) : ""
|
|
35
|
+
};
|
|
36
|
+
const paragraphs = [
|
|
37
|
+
fill(t("billing.email.invoice.greeting", "Hello {name},"), values),
|
|
38
|
+
fill(t("billing.email.invoice.body", "Please find attached invoice {number} for {amount}, due on {dueDate}."), values),
|
|
39
|
+
params.iban ? fill(t("billing.email.invoice.payment", "Please pay to IBAN {iban} with the transfer title {number}."), values) : "",
|
|
40
|
+
[t("billing.email.invoice.closing", "Kind regards,"), params.sellerName ?? ""].filter(Boolean).join("\n")
|
|
41
|
+
].filter(Boolean);
|
|
42
|
+
return {
|
|
43
|
+
subject: fill(t("billing.email.invoice.subject", "Invoice {number}"), values),
|
|
44
|
+
text: paragraphs.join("\n\n"),
|
|
45
|
+
html: paragraphs.map((p) => `<p>${escapeHtml(p).replace(/\n/g, "<br/>")}</p>`).join("")
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function defaultSendEmail(options) {
|
|
49
|
+
const { sendEmail } = await import("@open-mercato/shared/lib/email/send");
|
|
50
|
+
await sendEmail(options);
|
|
51
|
+
}
|
|
52
|
+
async function appendDelivery(em, invoice, delivery) {
|
|
53
|
+
await em.execute(
|
|
54
|
+
`UPDATE billing_invoices
|
|
55
|
+
SET metadata = jsonb_set(COALESCE(metadata, '{}'::jsonb), '{deliveries}',
|
|
56
|
+
COALESCE(metadata->'deliveries', '[]'::jsonb) || ?::jsonb),
|
|
57
|
+
updated_at = now()
|
|
58
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ?`,
|
|
59
|
+
[JSON.stringify([delivery]), invoice.id, invoice.tenantId, invoice.organizationId]
|
|
60
|
+
);
|
|
61
|
+
const deliveries = Array.isArray(invoice.metadata?.deliveries) ? invoice.metadata?.deliveries : [];
|
|
62
|
+
invoice.metadata = { ...invoice.metadata ?? {}, deliveries: [...deliveries, delivery] };
|
|
63
|
+
}
|
|
64
|
+
async function sendInvoice(em, params, deps = {}) {
|
|
65
|
+
const scope = { tenantId: params.tenantId, organizationId: params.organizationId };
|
|
66
|
+
const invoice = await em.findOne(BillingInvoice, { id: params.invoiceId, ...scope, deletedAt: null });
|
|
67
|
+
if (!invoice) throw new CrudHttpError(404, { error: "Invoice not found" });
|
|
68
|
+
if (invoice.status === "draft" || invoice.testRun) {
|
|
69
|
+
throw new CrudHttpError(409, {
|
|
70
|
+
error: "Only posted invoices can be sent",
|
|
71
|
+
code: "billing.invoice.not_posted"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const account = await em.findOne(BillingAccount, { id: invoice.billAccountId, ...scope });
|
|
75
|
+
const buyer = invoice.buyerSnapshot ?? {};
|
|
76
|
+
const to = account?.invoiceEmail ?? (typeof buyer.invoice_email === "string" ? buyer.invoice_email : "");
|
|
77
|
+
if (!isDeliverableEmail(to)) {
|
|
78
|
+
throw new CrudHttpError(422, {
|
|
79
|
+
error: "The billing account has no deliverable invoice email",
|
|
80
|
+
code: "billing.invoice.email_invalid"
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const language = typeof buyer.invoice_language === "string" && buyer.invoice_language || account?.invoiceLanguage || DEFAULT_INVOICE_LANGUAGE;
|
|
84
|
+
const translate = await (deps.loadTranslator ?? loadDocumentTranslator)(language);
|
|
85
|
+
const seller = invoice.sellerSnapshot ?? {};
|
|
86
|
+
const email = buildInvoiceEmail({
|
|
87
|
+
invoice,
|
|
88
|
+
buyerName: typeof buyer.name === "string" && buyer.name || account?.name || "",
|
|
89
|
+
sellerName: typeof seller.legal_name === "string" ? seller.legal_name : null,
|
|
90
|
+
iban: typeof seller.iban === "string" ? seller.iban : null,
|
|
91
|
+
language,
|
|
92
|
+
translate
|
|
93
|
+
});
|
|
94
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
95
|
+
let error = null;
|
|
96
|
+
try {
|
|
97
|
+
const pdf = await getInvoicePdf(em, invoice, { loadTranslator: deps.loadTranslator });
|
|
98
|
+
await (deps.sendEmail ?? defaultSendEmail)({
|
|
99
|
+
to,
|
|
100
|
+
subject: email.subject,
|
|
101
|
+
text: email.text,
|
|
102
|
+
html: email.html,
|
|
103
|
+
attachments: [{ filename: pdf.fileName, content: Buffer.from(pdf.bytes).toString("base64"), contentType: "application/pdf" }],
|
|
104
|
+
tenantId: params.tenantId,
|
|
105
|
+
organizationId: params.organizationId
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
error = err instanceof Error ? err.message : String(err);
|
|
109
|
+
console.warn("[billing.invoice.send] delivery failed", invoice.id, error);
|
|
110
|
+
}
|
|
111
|
+
const status = error ? "failed" : "sent";
|
|
112
|
+
await appendDelivery(em, invoice, { at: now().toISOString(), to, status, error, trigger: params.trigger });
|
|
113
|
+
await emitBillingEvent(status === "sent" ? "billing.invoice.sent" : "billing.invoice.send_failed", {
|
|
114
|
+
invoiceId: invoice.id,
|
|
115
|
+
invoiceNumber: invoice.number,
|
|
116
|
+
billAccountId: invoice.billAccountId,
|
|
117
|
+
to,
|
|
118
|
+
trigger: params.trigger,
|
|
119
|
+
error,
|
|
120
|
+
tenantId: invoice.tenantId,
|
|
121
|
+
organizationId: invoice.organizationId
|
|
122
|
+
});
|
|
123
|
+
return { invoiceId: invoice.id, to, status, error };
|
|
124
|
+
}
|
|
125
|
+
export {
|
|
126
|
+
buildInvoiceEmail,
|
|
127
|
+
hasSuccessfulDelivery,
|
|
128
|
+
isDeliverableEmail,
|
|
129
|
+
sendInvoice
|
|
130
|
+
};
|
|
131
|
+
//# sourceMappingURL=invoiceDelivery.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/invoiceDelivery.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { SendEmailOptions } from '@open-mercato/shared/lib/email/send'\nimport { BillingAccount, BillingInvoice } from '../data/entities'\nimport { emitBillingEvent } from '../events'\nimport { DEFAULT_INVOICE_LANGUAGE } from './invoiceLanguages'\nimport { formatIban } from './iban'\nimport { documentLocale, formatDocumentDate, formatDocumentMoney, type DocumentTranslate } from './pdf/invoiceModel'\nimport { getInvoicePdf, loadDocumentTranslator, type TranslatorLoader } from './pdf/invoicePdf'\n\n/**\n * Emails a posted billing document to the account's invoice address with\n * the frozen PDF attached (Spec B), through the platform's standard\n * `sendEmail()` \u2014 no custom transport.\n *\n * - posted (non-draft, non-test) documents only \u2192 409 `billing.invoice.not_posted`;\n * - the connector's placeholder addresses (`@invalid.local`) and malformed\n * emails are refused \u2192 422 `billing.invoice.email_invalid`, nothing sent;\n * - every attempt appends `{ at, to, status, error?, trigger }` to\n * `metadata.deliveries` (atomic jsonb append \u2014 concurrent sends never\n * lose an entry) and emits `billing.invoice.sent` / `send_failed`;\n * - transport errors (e.g. `EMAIL_TRANSPORT_NOT_CONFIGURED`) are recorded\n * as a `failed` delivery and returned, never thrown \u2014 posting is\n * unaffected and the operator sees the reason.\n */\n\nexport type DeliveryTrigger = 'manual' | 'auto'\n\nexport type InvoiceDelivery = {\n at: string\n to: string\n status: 'sent' | 'failed'\n error?: string | null\n trigger: DeliveryTrigger\n}\n\nexport type SendInvoiceResult = { invoiceId: string; to: string; status: 'sent' | 'failed'; error: string | null }\n\nexport type SendEmailFn = (options: SendEmailOptions) => Promise<void>\n\nexport type SendInvoiceDeps = {\n sendEmail?: SendEmailFn\n loadTranslator?: TranslatorLoader\n now?: () => Date\n}\n\nconst PLACEHOLDER_DOMAIN = '@invalid.local'\nconst emailSchema = z.string().trim().email()\n\nexport function isDeliverableEmail(value: string | null | undefined): value is string {\n if (!value) return false\n if (value.trim().toLowerCase().endsWith(PLACEHOLDER_DOMAIN)) return false\n return emailSchema.safeParse(value).success\n}\n\nexport function hasSuccessfulDelivery(\n invoice: Pick<BillingInvoice, 'metadata'>,\n trigger?: DeliveryTrigger,\n): boolean {\n const deliveries = (invoice.metadata?.deliveries ?? []) as InvoiceDelivery[]\n return Array.isArray(deliveries) && deliveries.some((d) => d.status === 'sent' && (!trigger || d.trigger === trigger))\n}\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' })[c] ?? c)\n}\n\nfunction fill(template: string, params: Record<string, string>): string {\n return template.replace(/\\{(\\w+)\\}/g, (match, key: string) => params[key] ?? match)\n}\n\n/** Subject + plain-text and HTML bodies in the document language. Pure. */\nexport function buildInvoiceEmail(params: {\n invoice: Pick<BillingInvoice, 'number' | 'totalGross' | 'currencyCode' | 'dueDate'>\n buyerName: string\n sellerName: string | null\n iban: string | null\n language: string\n translate: DocumentTranslate\n}): { subject: string; text: string; html: string } {\n const { invoice, translate: t } = params\n const locale = documentLocale(params.language)\n const values = {\n number: invoice.number,\n name: params.buyerName,\n amount: formatDocumentMoney(invoice.totalGross, invoice.currencyCode, locale).replace(/[\u00A0\u202F]/g, ' '),\n dueDate: formatDocumentDate(invoice.dueDate ?? null, locale),\n iban: params.iban ? formatIban(params.iban) : '',\n }\n const paragraphs = [\n fill(t('billing.email.invoice.greeting', 'Hello {name},'), values),\n fill(t('billing.email.invoice.body', 'Please find attached invoice {number} for {amount}, due on {dueDate}.'), values),\n params.iban\n ? fill(t('billing.email.invoice.payment', 'Please pay to IBAN {iban} with the transfer title {number}.'), values)\n : '',\n [t('billing.email.invoice.closing', 'Kind regards,'), params.sellerName ?? ''].filter(Boolean).join('\\n'),\n ].filter(Boolean)\n return {\n subject: fill(t('billing.email.invoice.subject', 'Invoice {number}'), values),\n text: paragraphs.join('\\n\\n'),\n html: paragraphs.map((p) => `<p>${escapeHtml(p).replace(/\\n/g, '<br/>')}</p>`).join(''),\n }\n}\n\nasync function defaultSendEmail(options: SendEmailOptions): Promise<void> {\n const { sendEmail } = await import('@open-mercato/shared/lib/email/send')\n await sendEmail(options)\n}\n\nasync function appendDelivery(em: EntityManager, invoice: BillingInvoice, delivery: InvoiceDelivery): Promise<void> {\n await em.execute(\n `UPDATE billing_invoices\n SET metadata = jsonb_set(COALESCE(metadata, '{}'::jsonb), '{deliveries}',\n COALESCE(metadata->'deliveries', '[]'::jsonb) || ?::jsonb),\n updated_at = now()\n WHERE id = ? AND tenant_id = ? AND organization_id = ?`,\n [JSON.stringify([delivery]), invoice.id, invoice.tenantId, invoice.organizationId],\n )\n const deliveries = Array.isArray(invoice.metadata?.deliveries) ? (invoice.metadata?.deliveries as InvoiceDelivery[]) : []\n invoice.metadata = { ...(invoice.metadata ?? {}), deliveries: [...deliveries, delivery] }\n}\n\nexport async function sendInvoice(\n em: EntityManager,\n params: { invoiceId: string; tenantId: string; organizationId: string; trigger: DeliveryTrigger },\n deps: SendInvoiceDeps = {},\n): Promise<SendInvoiceResult> {\n const scope = { tenantId: params.tenantId, organizationId: params.organizationId }\n const invoice = await em.findOne(BillingInvoice, { id: params.invoiceId, ...scope, deletedAt: null } as never)\n if (!invoice) throw new CrudHttpError(404, { error: 'Invoice not found' })\n if (invoice.status === 'draft' || invoice.testRun) {\n throw new CrudHttpError(409, {\n error: 'Only posted invoices can be sent',\n code: 'billing.invoice.not_posted',\n })\n }\n\n // The live account address wins (an operator may fix a bad address and\n // resend); the frozen buyer snapshot is the fallback for a deleted account.\n const account = await em.findOne(BillingAccount, { id: invoice.billAccountId, ...scope } as never)\n const buyer = (invoice.buyerSnapshot ?? {}) as Record<string, unknown>\n const to = account?.invoiceEmail ?? (typeof buyer.invoice_email === 'string' ? buyer.invoice_email : '')\n if (!isDeliverableEmail(to)) {\n throw new CrudHttpError(422, {\n error: 'The billing account has no deliverable invoice email',\n code: 'billing.invoice.email_invalid',\n })\n }\n\n const language =\n (typeof buyer.invoice_language === 'string' && buyer.invoice_language) ||\n account?.invoiceLanguage ||\n DEFAULT_INVOICE_LANGUAGE\n const translate = await (deps.loadTranslator ?? loadDocumentTranslator)(language)\n const seller = (invoice.sellerSnapshot ?? {}) as Record<string, unknown>\n const email = buildInvoiceEmail({\n invoice,\n buyerName: (typeof buyer.name === 'string' && buyer.name) || account?.name || '',\n sellerName: typeof seller.legal_name === 'string' ? seller.legal_name : null,\n iban: typeof seller.iban === 'string' ? seller.iban : null,\n language,\n translate,\n })\n\n const now = deps.now ?? (() => new Date())\n let error: string | null = null\n try {\n const pdf = await getInvoicePdf(em, invoice, { loadTranslator: deps.loadTranslator })\n await (deps.sendEmail ?? defaultSendEmail)({\n to,\n subject: email.subject,\n text: email.text,\n html: email.html,\n attachments: [{ filename: pdf.fileName, content: Buffer.from(pdf.bytes).toString('base64'), contentType: 'application/pdf' }],\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n })\n } catch (err) {\n error = err instanceof Error ? err.message : String(err)\n console.warn('[billing.invoice.send] delivery failed', invoice.id, error)\n }\n\n const status: InvoiceDelivery['status'] = error ? 'failed' : 'sent'\n await appendDelivery(em, invoice, { at: now().toISOString(), to, status, error, trigger: params.trigger })\n await emitBillingEvent(status === 'sent' ? 'billing.invoice.sent' : 'billing.invoice.send_failed', {\n invoiceId: invoice.id,\n invoiceNumber: invoice.number,\n billAccountId: invoice.billAccountId,\n to,\n trigger: params.trigger,\n error,\n tenantId: invoice.tenantId,\n organizationId: invoice.organizationId,\n })\n return { invoiceId: invoice.id, to, status, error }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const SUPPORTED_INVOICE_LANGUAGES = [
|
|
2
|
+
{ code: "en", label: "English" },
|
|
3
|
+
{ code: "pl", label: "Polski" }
|
|
4
|
+
];
|
|
5
|
+
const INVOICE_LANGUAGE_CODES = SUPPORTED_INVOICE_LANGUAGES.map((l) => l.code);
|
|
6
|
+
const DEFAULT_INVOICE_LANGUAGE = "en";
|
|
7
|
+
function isSupportedInvoiceLanguage(value) {
|
|
8
|
+
return INVOICE_LANGUAGE_CODES.includes(value);
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_INVOICE_LANGUAGE,
|
|
12
|
+
INVOICE_LANGUAGE_CODES,
|
|
13
|
+
SUPPORTED_INVOICE_LANGUAGES,
|
|
14
|
+
isSupportedInvoiceLanguage
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=invoiceLanguages.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/invoiceLanguages.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Invoice languages the built-in billing document template can render.\n *\n * Single source of truth for the account form's language picklist and the\n * `invoiceLanguage` validator. Adding a language means adding the template\n * labels for it AND one entry here \u2014 never one without the other.\n *\n * `label` is the language's own name (endonym), so it reads correctly no\n * matter which UI locale the operator uses \u2014 it is intentionally not routed\n * through i18n.\n *\n * Spec: .ai/specs/2026-11-14-billing-account-workspace.md (D22). Narrowing the\n * list per organization (`BillingSettings.enabled_invoice_languages`) arrives\n * with the standalone-ledger spec; until then every supported language is\n * selectable.\n */\n\nexport const SUPPORTED_INVOICE_LANGUAGES = [\n { code: 'en', label: 'English' },\n { code: 'pl', label: 'Polski' },\n] as const\n\nexport type InvoiceLanguageCode = (typeof SUPPORTED_INVOICE_LANGUAGES)[number]['code']\n\nexport const INVOICE_LANGUAGE_CODES = SUPPORTED_INVOICE_LANGUAGES.map((l) => l.code) as [\n InvoiceLanguageCode,\n ...InvoiceLanguageCode[],\n]\n\nexport const DEFAULT_INVOICE_LANGUAGE: InvoiceLanguageCode = 'en'\n\nexport function isSupportedInvoiceLanguage(value: string): value is InvoiceLanguageCode {\n return (INVOICE_LANGUAGE_CODES as readonly string[]).includes(value)\n}\n"],
|
|
5
|
+
"mappings": "AAiBO,MAAM,8BAA8B;AAAA,EACzC,EAAE,MAAM,MAAM,OAAO,UAAU;AAAA,EAC/B,EAAE,MAAM,MAAM,OAAO,SAAS;AAChC;AAIO,MAAM,yBAAyB,4BAA4B,IAAI,CAAC,MAAM,EAAE,IAAI;AAK5E,MAAM,2BAAgD;AAEtD,SAAS,2BAA2B,OAA6C;AACtF,SAAQ,uBAA6C,SAAS,KAAK;AACrE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,118 +1,86 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "
|
|
5
|
-
import {
|
|
6
|
-
__resetInvoiceStatusCacheForTests,
|
|
7
|
-
resolveInvoiceStatusEntryId
|
|
8
|
-
} from "./invoiceStatus.js";
|
|
9
|
-
const DRAFT_INVOICE_STATUS_VALUE = "draft";
|
|
1
|
+
import { BillingInvoice, BillingInvoiceLine } from "../data/entities.js";
|
|
2
|
+
import { draftNumber, testNumber } from "./numbering.js";
|
|
3
|
+
import { sumTotals } from "./vat.js";
|
|
4
|
+
import { lineVat, priceBasisOf, unitPriceNetOf } from "./lineAmounts.js";
|
|
10
5
|
function __resetInvoiceWriterCacheForTests() {
|
|
11
|
-
__resetInvoiceStatusCacheForTests();
|
|
12
|
-
}
|
|
13
|
-
function generateTestInvoiceNumber() {
|
|
14
|
-
const random = Math.floor(Math.random() * 1e9).toString(16).padStart(8, "0");
|
|
15
|
-
return `TEST-${Date.now().toString(16)}-${random}`;
|
|
16
6
|
}
|
|
17
7
|
function roundHalfUp2dp(value) {
|
|
18
|
-
|
|
8
|
+
const sign = value < 0 ? -1 : 1;
|
|
9
|
+
return sign * Math.round((Math.abs(value) + Number.EPSILON) * 100) / 100;
|
|
19
10
|
}
|
|
20
11
|
function format4dp(value) {
|
|
21
12
|
return roundHalfUp2dp(value).toFixed(4);
|
|
22
13
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
total += Number.parseFloat(line.totalNetAmount);
|
|
27
|
-
}
|
|
28
|
-
return format4dp(total);
|
|
14
|
+
function itemTaxRate(item) {
|
|
15
|
+
const rate = Number.parseFloat(String(item.taxRate ?? "0"));
|
|
16
|
+
return Number.isFinite(rate) && rate >= 0 ? String(rate) : "0";
|
|
29
17
|
}
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
function isoDate(date) {
|
|
19
|
+
return new Date(date).toISOString().slice(0, 10);
|
|
20
|
+
}
|
|
21
|
+
async function writeDraftInvoice(em, params) {
|
|
22
|
+
const amounts = params.lines.map(
|
|
23
|
+
(line) => lineVat({ amount: line.lineAmount, priceBasis: line.priceBasis, taxRate: line.taxRate })
|
|
24
|
+
);
|
|
25
|
+
const totals = sumTotals(amounts);
|
|
32
26
|
if (params.mode === "dry") {
|
|
33
|
-
return { invoiceId: null, invoiceNumber: null, totalNet };
|
|
27
|
+
return { invoiceId: null, invoiceNumber: null, totalNet: totals.subtotalNet, totalGross: totals.totalGross };
|
|
34
28
|
}
|
|
35
29
|
const { account, period, billRunId, lines, mode } = params;
|
|
36
|
-
|
|
37
|
-
if (mode === "test") {
|
|
38
|
-
invoiceNumber = generateTestInvoiceNumber();
|
|
39
|
-
} else {
|
|
40
|
-
const numberGenerator = container.resolve(
|
|
41
|
-
"salesDocumentNumberGenerator"
|
|
42
|
-
);
|
|
43
|
-
const generated = await numberGenerator.generate({
|
|
44
|
-
organizationId: account.organizationId,
|
|
45
|
-
tenantId: account.tenantId,
|
|
46
|
-
kind: "invoice"
|
|
47
|
-
});
|
|
48
|
-
invoiceNumber = generated.number;
|
|
49
|
-
}
|
|
50
|
-
const statusEntryId = await resolveInvoiceStatusEntryId(
|
|
51
|
-
em,
|
|
52
|
-
account.tenantId,
|
|
53
|
-
account.organizationId,
|
|
54
|
-
"draft"
|
|
55
|
-
);
|
|
30
|
+
const invoiceNumber = mode === "test" ? testNumber() : draftNumber();
|
|
56
31
|
const now = /* @__PURE__ */ new Date();
|
|
57
|
-
const invoice = em.create(
|
|
32
|
+
const invoice = em.create(BillingInvoice, {
|
|
58
33
|
organizationId: account.organizationId,
|
|
59
34
|
tenantId: account.tenantId,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
35
|
+
billAccountId: account.id,
|
|
36
|
+
documentType: "invoice",
|
|
37
|
+
number: invoiceNumber,
|
|
38
|
+
series: null,
|
|
39
|
+
status: "draft",
|
|
40
|
+
testRun: mode === "test",
|
|
41
|
+
billRunId,
|
|
42
|
+
billDate: params.billDate,
|
|
43
|
+
periodStart: period.periodStart,
|
|
44
|
+
periodEnd: period.periodEnd,
|
|
64
45
|
currencyCode: account.currencyCode,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
grandTotalGrossAmount: totalNet,
|
|
72
|
-
paidTotalAmount: "0",
|
|
73
|
-
outstandingAmount: totalNet,
|
|
74
|
-
metadata: {
|
|
75
|
-
bill_run_id: billRunId,
|
|
76
|
-
bill_account_id: account.id,
|
|
77
|
-
bill_period_start: period.periodStart.toISOString().slice(0, 10),
|
|
78
|
-
bill_period_end: period.periodEnd.toISOString().slice(0, 10),
|
|
79
|
-
test_run: mode === "test" ? true : void 0
|
|
80
|
-
},
|
|
46
|
+
subtotalNet: totals.subtotalNet,
|
|
47
|
+
taxTotal: totals.taxTotal,
|
|
48
|
+
totalGross: totals.totalGross,
|
|
49
|
+
paidTotal: "0",
|
|
50
|
+
creditedTotal: "0",
|
|
51
|
+
outstanding: totals.totalGross,
|
|
81
52
|
createdAt: now,
|
|
82
53
|
updatedAt: now
|
|
83
54
|
});
|
|
84
55
|
em.persist(invoice);
|
|
85
|
-
let lineNumber = 1;
|
|
86
|
-
for (const line of lines) {
|
|
87
|
-
const invoiceLine = em.create(SalesInvoiceLine, {
|
|
88
|
-
invoice,
|
|
89
|
-
organizationId: account.organizationId,
|
|
90
|
-
tenantId: account.tenantId,
|
|
91
|
-
lineNumber: lineNumber++,
|
|
92
|
-
kind: "product",
|
|
93
|
-
description: line.description,
|
|
94
|
-
quantity: line.quantity,
|
|
95
|
-
normalizedQuantity: line.quantity,
|
|
96
|
-
currencyCode: account.currencyCode,
|
|
97
|
-
unitPriceNet: line.unitPriceNet,
|
|
98
|
-
unitPriceGross: line.unitPriceNet,
|
|
99
|
-
// gross == net until tax computed on post
|
|
100
|
-
discountAmount: "0",
|
|
101
|
-
discountPercent: "0",
|
|
102
|
-
taxRate: "0",
|
|
103
|
-
taxAmount: "0",
|
|
104
|
-
totalNetAmount: line.totalNetAmount,
|
|
105
|
-
totalGrossAmount: line.totalNetAmount,
|
|
106
|
-
metadata: {
|
|
107
|
-
billing_item_id: line.billingItemId,
|
|
108
|
-
billing_type: line.billingType,
|
|
109
|
-
...line.metadataExtra ?? {}
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
em.persist(invoiceLine);
|
|
113
|
-
}
|
|
114
56
|
await em.flush();
|
|
115
|
-
|
|
57
|
+
lines.forEach((line, index) => {
|
|
58
|
+
const amount = amounts[index];
|
|
59
|
+
em.persist(
|
|
60
|
+
em.create(BillingInvoiceLine, {
|
|
61
|
+
organizationId: account.organizationId,
|
|
62
|
+
tenantId: account.tenantId,
|
|
63
|
+
invoiceId: invoice.id,
|
|
64
|
+
lineNumber: index + 1,
|
|
65
|
+
billingItemId: line.billingItemId,
|
|
66
|
+
billingType: line.billingType,
|
|
67
|
+
description: line.description,
|
|
68
|
+
quantity: line.quantity,
|
|
69
|
+
unitPriceNet: line.priceBasis === "gross" ? unitPriceNetOf(line.unitPrice, "gross", line.taxRate) : line.unitPrice,
|
|
70
|
+
taxRate: line.taxRate,
|
|
71
|
+
netAmount: format4dp(amount.net),
|
|
72
|
+
taxAmount: format4dp(amount.tax),
|
|
73
|
+
grossAmount: format4dp(amount.gross),
|
|
74
|
+
servicePeriodStart: line.servicePeriodStart ?? null,
|
|
75
|
+
servicePeriodEnd: line.servicePeriodEnd ?? null,
|
|
76
|
+
metadata: line.metadataExtra ?? null,
|
|
77
|
+
createdAt: now,
|
|
78
|
+
updatedAt: now
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
await em.flush();
|
|
83
|
+
return { invoiceId: invoice.id, invoiceNumber, totalNet: totals.subtotalNet, totalGross: totals.totalGross };
|
|
116
84
|
}
|
|
117
85
|
function buildInvoiceLinesFromItems(items) {
|
|
118
86
|
const lines = [];
|
|
@@ -125,9 +93,11 @@ function buildInvoiceLinesFromItems(items) {
|
|
|
125
93
|
billingItemId: item.id,
|
|
126
94
|
billingType: "one_time",
|
|
127
95
|
description: item.description,
|
|
128
|
-
|
|
96
|
+
unitPrice: amount,
|
|
129
97
|
quantity: "1",
|
|
130
|
-
|
|
98
|
+
lineAmount: amount,
|
|
99
|
+
priceBasis: priceBasisOf(item.priceBasis),
|
|
100
|
+
taxRate: itemTaxRate(item)
|
|
131
101
|
});
|
|
132
102
|
} else if (item.type === "recurring") {
|
|
133
103
|
const rate = item.rateJson;
|
|
@@ -137,17 +107,55 @@ function buildInvoiceLinesFromItems(items) {
|
|
|
137
107
|
billingItemId: item.id,
|
|
138
108
|
billingType: "recurring",
|
|
139
109
|
description: item.description,
|
|
140
|
-
|
|
110
|
+
unitPrice,
|
|
141
111
|
quantity: "1",
|
|
142
|
-
|
|
112
|
+
lineAmount: unitPrice,
|
|
113
|
+
priceBasis: priceBasisOf(item.priceBasis),
|
|
114
|
+
taxRate: itemTaxRate(item)
|
|
143
115
|
});
|
|
144
116
|
}
|
|
145
117
|
}
|
|
146
118
|
return lines;
|
|
147
119
|
}
|
|
120
|
+
function displayPeriod(lines, fallback) {
|
|
121
|
+
let start = null;
|
|
122
|
+
let end = null;
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
if (line.servicePeriodStart && (!start || line.servicePeriodStart < start)) start = line.servicePeriodStart;
|
|
125
|
+
if (line.servicePeriodEnd && (!end || line.servicePeriodEnd > end)) end = line.servicePeriodEnd;
|
|
126
|
+
}
|
|
127
|
+
return { periodStart: start ?? fallback.periodStart, periodEnd: end ?? fallback.periodEnd };
|
|
128
|
+
}
|
|
129
|
+
function buildRecurringLineDescriptors(item, lines) {
|
|
130
|
+
return lines.map((line) => {
|
|
131
|
+
const amount = format4dp(line.amount);
|
|
132
|
+
const dated = line.proration !== null || line.kind === "credit";
|
|
133
|
+
return {
|
|
134
|
+
billingItemId: item.id,
|
|
135
|
+
billingType: "recurring",
|
|
136
|
+
description: dated ? `${item.description} \xB7 ${isoDate(line.servicePeriodStart)} \u2013 ${isoDate(line.servicePeriodEnd)}` : item.description,
|
|
137
|
+
unitPrice: amount,
|
|
138
|
+
quantity: "1",
|
|
139
|
+
lineAmount: amount,
|
|
140
|
+
// Proration and credit lines inherit the item's rate (Spec B) and basis (Spec F).
|
|
141
|
+
priceBasis: priceBasisOf(item.priceBasis),
|
|
142
|
+
taxRate: itemTaxRate(item),
|
|
143
|
+
servicePeriodStart: line.servicePeriodStart,
|
|
144
|
+
servicePeriodEnd: line.servicePeriodEnd,
|
|
145
|
+
metadataExtra: {
|
|
146
|
+
charge_timing: item.chargeTiming,
|
|
147
|
+
...line.proration ? { proration: { active_days: line.proration.activeDays, period_days: line.proration.periodDays } } : {},
|
|
148
|
+
...line.kind === "credit" ? { credit_for: item.id } : {}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
148
153
|
export {
|
|
149
154
|
__resetInvoiceWriterCacheForTests,
|
|
150
155
|
buildInvoiceLinesFromItems,
|
|
156
|
+
buildRecurringLineDescriptors,
|
|
157
|
+
displayPeriod,
|
|
158
|
+
itemTaxRate,
|
|
151
159
|
writeDraftInvoice
|
|
152
160
|
};
|
|
153
161
|
//# sourceMappingURL=invoiceWriter.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/lib/invoiceWriter.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport {\n SalesInvoice,\n SalesInvoiceLine,\n} from '@open-mercato/core/modules/sales/data/entities'\nimport type { SalesDocumentNumberGenerator } from '@open-mercato/core/modules/sales/services/salesDocumentNumberGenerator'\nimport type { BillingAccount, BillingItem } from '../data/entities'\nimport type { BillPeriod } from './billPeriod'\nimport {\n __resetInvoiceStatusCacheForTests,\n resolveInvoiceStatusEntryId,\n} from './invoiceStatus'\n\n/**\n * Writes a `core/sales` draft invoice for a billing account's bill period.\n *\n * Two notable deviations from the spec, documented in CHANGELOG / spec:\n *\n * 1. `SalesInvoice.invoice_number` is `NOT NULL` in the upstream\n * schema, but the spec says \"sequence number assigned at Post.\"\n * Real-mode invoices get a real number via\n * `salesDocumentNumberGenerator.generate({ kind: 'invoice' })` at\n * *draft create time* \u2014 the spec's intent (gap-free numbering via\n * atomic SQL upsert) is preserved; the timing differs because the\n * upstream column is `NOT NULL`. \"Post\" then just transitions\n * status `draft \u2192 posted`.\n *\n * 2. Test-mode invoices use a `TEST-<uuid>` placeholder number (not\n * the real sequence) so test runs do not burn production numbers\n * and can be repeated idempotently.\n *\n * VAT is NOT computed here \u2014 `core/sales`'s tax service owns the\n * per-line `tax_rate` / `tax_amount` calculation, and runs on POST.\n * Drafts therefore carry zeroes in the tax columns; the totals reflect\n * net amounts only.\n */\n\nexport type WriteMode = 'real' | 'test' | 'dry'\n\nexport type InvoiceLineDescriptor = {\n /** Maps to billing_item_id on the resulting SalesInvoiceLine.metadata. */\n billingItemId: string\n /** From the spec's `metadata.billing_type` for traceability. */\n billingType: 'one_time' | 'recurring' | 'usage'\n description: string\n /** Net unit price (numeric string for SalesInvoiceLine.unit_price_net). */\n unitPriceNet: string\n /** Quantity (string for the numeric(18,4) column). */\n quantity: string\n /** Total net (string). */\n totalNetAmount: string\n /** Optional per-line metadata extension \u2014 e.g. usage tier breakdown. */\n metadataExtra?: Record<string, unknown>\n}\n\nexport type WriteInvoiceParams = {\n mode: WriteMode\n account: BillingAccount\n period: BillPeriod\n billRunId: string\n lines: InvoiceLineDescriptor[]\n}\n\nexport type WrittenInvoice = {\n invoiceId: string | null\n invoiceNumber: string | null\n totalNet: string\n}\n\nconst DRAFT_INVOICE_STATUS_VALUE = 'draft'\n\n/**\n * Test hook \u2014 clears the shared resolver's status cache so the engine\n * test suite stays deterministic across mocked-EM environments.\n */\nexport function __resetInvoiceWriterCacheForTests(): void {\n __resetInvoiceStatusCacheForTests()\n}\n\nfunction generateTestInvoiceNumber(): string {\n // Match the pattern operators search by: `TEST-` prefix + random\n // suffix so the test wipe-out endpoint can filter via `LIKE 'TEST-%'`\n // (defensive \u2014 primary filter is `metadata.test_run=true`).\n const random = Math.floor(Math.random() * 1e9)\n .toString(16)\n .padStart(8, '0')\n return `TEST-${Date.now().toString(16)}-${random}`\n}\n\n/**\n * 2dp half-up rounding that survives binary floating-point edge cases\n * (e.g. `1.005 * 100 === 100.49999...` would otherwise round DOWN to\n * `1.00` under naive `Math.round`). The `+ EPSILON` nudge pushes\n * representations like 100.4999...8 up to the true half-up boundary.\n * Matches the spec's \"2 decimal places, half-up\" rounding policy.\n */\nfunction roundHalfUp2dp(value: number): number {\n return Math.round((value + Number.EPSILON) * 100) / 100\n}\n\nfunction format4dp(value: number): string {\n return roundHalfUp2dp(value).toFixed(4)\n}\n\nfunction sumNetTotal(lines: InvoiceLineDescriptor[]): string {\n let total = 0\n for (const line of lines) {\n total += Number.parseFloat(line.totalNetAmount)\n }\n return format4dp(total)\n}\n\nexport async function writeDraftInvoice(\n em: EntityManager,\n container: AwilixContainer,\n params: WriteInvoiceParams,\n): Promise<WrittenInvoice> {\n const totalNet = sumNetTotal(params.lines)\n\n if (params.mode === 'dry') {\n // Compute totals; persist nothing.\n return { invoiceId: null, invoiceNumber: null, totalNet }\n }\n\n const { account, period, billRunId, lines, mode } = params\n\n let invoiceNumber: string\n if (mode === 'test') {\n invoiceNumber = generateTestInvoiceNumber()\n } else {\n const numberGenerator = container.resolve(\n 'salesDocumentNumberGenerator',\n ) as SalesDocumentNumberGenerator\n const generated = await numberGenerator.generate({\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n kind: 'invoice',\n })\n invoiceNumber = generated.number\n }\n\n const statusEntryId = await resolveInvoiceStatusEntryId(\n em,\n account.tenantId,\n account.organizationId,\n 'draft',\n )\n\n const now = new Date()\n const invoice = em.create(SalesInvoice, {\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n invoiceNumber,\n statusEntryId,\n status: DRAFT_INVOICE_STATUS_VALUE,\n issueDate: now,\n currencyCode: account.currencyCode,\n subtotalNetAmount: totalNet,\n subtotalGrossAmount: totalNet, // gross == net for drafts (tax computed on post)\n discountTotalAmount: '0',\n taxTotalAmount: '0',\n grandTotalNetAmount: totalNet,\n grandTotalGrossAmount: totalNet,\n paidTotalAmount: '0',\n outstandingAmount: totalNet,\n metadata: {\n bill_run_id: billRunId,\n bill_account_id: account.id,\n bill_period_start: period.periodStart.toISOString().slice(0, 10),\n bill_period_end: period.periodEnd.toISOString().slice(0, 10),\n test_run: mode === 'test' ? true : undefined,\n },\n createdAt: now,\n updatedAt: now,\n })\n em.persist(invoice)\n\n let lineNumber = 1\n for (const line of lines) {\n const invoiceLine = em.create(SalesInvoiceLine, {\n invoice,\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n lineNumber: lineNumber++,\n kind: 'product',\n description: line.description,\n quantity: line.quantity,\n normalizedQuantity: line.quantity,\n currencyCode: account.currencyCode,\n unitPriceNet: line.unitPriceNet,\n unitPriceGross: line.unitPriceNet, // gross == net until tax computed on post\n discountAmount: '0',\n discountPercent: '0',\n taxRate: '0',\n taxAmount: '0',\n totalNetAmount: line.totalNetAmount,\n totalGrossAmount: line.totalNetAmount,\n metadata: {\n billing_item_id: line.billingItemId,\n billing_type: line.billingType,\n ...(line.metadataExtra ?? {}),\n },\n })\n em.persist(invoiceLine)\n }\n\n await em.flush()\n\n return { invoiceId: invoice.id, invoiceNumber, totalNet }\n}\n\n/**\n * Build the InvoiceLineDescriptor list for a single account's eligible\n * items (the result of `itemSelector.selectItemsForPeriod`). One line\n * per item; `usage` items are excluded earlier by the selector.\n *\n * Per-type pricing per the spec's `BillingItem \u2192 InvoiceLine mapping`:\n * - `one_time`: unit_price = amount, quantity = 1\n * - `recurring`: unit_price = unit_price, quantity = 1\n * (one full cycle per bill period; catch-up emits separate drafts\n * per missed cycle, never combined)\n */\nexport function buildInvoiceLinesFromItems(\n items: readonly BillingItem[],\n): InvoiceLineDescriptor[] {\n const lines: InvoiceLineDescriptor[] = []\n for (const item of items) {\n if (item.type === 'one_time') {\n const rate = item.rateJson as { amount?: number } | null\n if (!rate || typeof rate.amount !== 'number') continue\n // For one_time the per-unit price IS the total \u2014 quantity is 1.\n // Both fields therefore carry the same value, with the 2dp\n // half-up policy applied (unit price + line total are the same\n // numeric column at numeric(18,4)).\n const amount = format4dp(rate.amount)\n lines.push({\n billingItemId: item.id,\n billingType: 'one_time',\n description: item.description,\n unitPriceNet: amount,\n quantity: '1',\n totalNetAmount: amount,\n })\n } else if (item.type === 'recurring') {\n const rate = item.rateJson as { unit_price?: number } | null\n if (!rate || typeof rate.unit_price !== 'number') continue\n const unitPrice = format4dp(rate.unit_price)\n lines.push({\n billingItemId: item.id,\n billingType: 'recurring',\n description: item.description,\n unitPriceNet: unitPrice,\n quantity: '1',\n totalNetAmount: unitPrice,\n })\n }\n }\n return lines\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { BillingInvoice, BillingInvoiceLine } from '../data/entities'\nimport type { BillingAccount, BillingItem } from '../data/entities'\nimport type { BillPeriod } from './billPeriod'\nimport type { RecurringLine } from './servicePeriods'\nimport { draftNumber, testNumber } from './numbering'\nimport { sumTotals } from './vat'\nimport { lineVat, priceBasisOf, unitPriceNetOf, type PriceBasis } from './lineAmounts'\n\n/**\n * Writes a billing-owned draft invoice (`billing_invoices` +\n * `billing_invoice_lines`) for one bill date of an account \u2014 Spec 0: billing\n * no longer records anything in core `sales`.\n *\n * - Real drafts get a temporary `DRAFT-\u2026` number; the series number is\n * claimed only at post (gap-free numbering \u2014 no number is burnt by a\n * deleted draft).\n * - Test drafts get `TEST-\u2026` and `test_run = true`; they are never posted.\n * - Spec B (B-D2): every line carries its item's VAT rate; tax is rounded\n * per line (half-up, 2 dp) and totals are sums of the rounded lines.\n * - Spec F (F-D2): line amounts are on the item's `price_basis`; net / tax\n * / gross come from `lineVat`, so a typed gross is reproduced exactly.\n */\n\nexport type WriteMode = 'real' | 'test' | 'dry'\n\nexport type InvoiceLineDescriptor = {\n /** Maps to `billing_invoice_lines.billing_item_id`. */\n billingItemId: string\n /** From the spec's `metadata.billing_type` for traceability. */\n billingType: 'one_time' | 'recurring' | 'usage'\n description: string\n /** Unit price on `priceBasis` (numeric string). The writer derives `unit_price_net`. */\n unitPrice: string\n /** Quantity (string for the numeric(18,4) column). */\n quantity: string\n /** Extended line amount on `priceBasis` (unit \u00D7 quantity \u00D7 proration). */\n lineAmount: string\n /** Spec F: the basis `unitPrice` / `lineAmount` are on (the item's `price_basis`). */\n priceBasis: PriceBasis\n /** Spec B: VAT rate in percent (numeric string), from the item. */\n taxRate: string\n /** Optional per-line metadata extension \u2014 e.g. usage tier breakdown. */\n metadataExtra?: Record<string, unknown>\n /**\n * Spec A: inclusive service period the line covers (recurring + usage).\n * Written to line metadata and used for the invoice's display period.\n */\n servicePeriodStart?: Date\n servicePeriodEnd?: Date\n}\n\nexport type WriteInvoiceParams = {\n mode: WriteMode\n account: BillingAccount\n /** Spec A: the bill date this invoice is for \u2014 the dedupe key. */\n billDate: Date\n /** Display period: min/max of the line service periods (see `displayPeriod`). */\n period: BillPeriod\n billRunId: string\n lines: InvoiceLineDescriptor[]\n}\n\nexport type WrittenInvoice = {\n invoiceId: string | null\n invoiceNumber: string | null\n totalNet: string\n totalGross: string\n}\n\n/** @deprecated kept for test-suite compatibility; the writer has no cache any more. */\nexport function __resetInvoiceWriterCacheForTests(): void {}\n\n/** 2dp half-up rounding (survives 1.005-style binary edges). */\nfunction roundHalfUp2dp(value: number): number {\n const sign = value < 0 ? -1 : 1\n return (sign * Math.round((Math.abs(value) + Number.EPSILON) * 100)) / 100\n}\n\nfunction format4dp(value: number): string {\n return roundHalfUp2dp(value).toFixed(4)\n}\n\n/** VAT rate of an item as a numeric string; items predating Spec B read as 0. */\nexport function itemTaxRate(item: Pick<BillingItem, 'taxRate'>): string {\n const rate = Number.parseFloat(String(item.taxRate ?? '0'))\n return Number.isFinite(rate) && rate >= 0 ? String(rate) : '0'\n}\n\nfunction isoDate(date: Date): string {\n return new Date(date).toISOString().slice(0, 10)\n}\n\nexport async function writeDraftInvoice(\n em: EntityManager,\n params: WriteInvoiceParams,\n): Promise<WrittenInvoice> {\n const amounts = params.lines.map((line) =>\n lineVat({ amount: line.lineAmount, priceBasis: line.priceBasis, taxRate: line.taxRate }),\n )\n const totals = sumTotals(amounts)\n\n if (params.mode === 'dry') {\n // Compute totals; persist nothing.\n return { invoiceId: null, invoiceNumber: null, totalNet: totals.subtotalNet, totalGross: totals.totalGross }\n }\n\n const { account, period, billRunId, lines, mode } = params\n const invoiceNumber = mode === 'test' ? testNumber() : draftNumber()\n const now = new Date()\n\n const invoice = em.create(BillingInvoice, {\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n billAccountId: account.id,\n documentType: 'invoice',\n number: invoiceNumber,\n series: null,\n status: 'draft',\n testRun: mode === 'test',\n billRunId,\n billDate: params.billDate,\n periodStart: period.periodStart,\n periodEnd: period.periodEnd,\n currencyCode: account.currencyCode,\n subtotalNet: totals.subtotalNet,\n taxTotal: totals.taxTotal,\n totalGross: totals.totalGross,\n paidTotal: '0',\n creditedTotal: '0',\n outstanding: totals.totalGross,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(invoice)\n await em.flush() // need the invoice id for the lines\n\n lines.forEach((line, index) => {\n const amount = amounts[index]\n em.persist(\n em.create(BillingInvoiceLine, {\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n invoiceId: invoice.id,\n lineNumber: index + 1,\n billingItemId: line.billingItemId,\n billingType: line.billingType,\n description: line.description,\n quantity: line.quantity,\n unitPriceNet:\n line.priceBasis === 'gross' ? unitPriceNetOf(line.unitPrice, 'gross', line.taxRate) : line.unitPrice,\n taxRate: line.taxRate,\n netAmount: format4dp(amount.net),\n taxAmount: format4dp(amount.tax),\n grossAmount: format4dp(amount.gross),\n servicePeriodStart: line.servicePeriodStart ?? null,\n servicePeriodEnd: line.servicePeriodEnd ?? null,\n metadata: line.metadataExtra ?? null,\n createdAt: now,\n updatedAt: now,\n }),\n )\n })\n await em.flush()\n\n return { invoiceId: invoice.id, invoiceNumber, totalNet: totals.subtotalNet, totalGross: totals.totalGross }\n}\n\n/**\n * Build the InvoiceLineDescriptor list for a single account's eligible\n * items (the result of `itemSelector.selectItemsForPeriod`). One line\n * per item; `usage` items are excluded earlier by the selector.\n *\n * Per-type pricing per the spec's `BillingItem \u2192 InvoiceLine mapping`:\n * - `one_time`: unit_price = amount, quantity = 1\n * - `recurring`: unit_price = unit_price, quantity = 1\n * (one full cycle per bill period; catch-up emits separate drafts\n * per missed cycle, never combined)\n */\nexport function buildInvoiceLinesFromItems(\n items: readonly BillingItem[],\n): InvoiceLineDescriptor[] {\n const lines: InvoiceLineDescriptor[] = []\n for (const item of items) {\n if (item.type === 'one_time') {\n const rate = item.rateJson as { amount?: number } | null\n if (!rate || typeof rate.amount !== 'number') continue\n // For one_time the per-unit price IS the total \u2014 quantity is 1.\n // Both fields therefore carry the same value, with the 2dp\n // half-up policy applied (unit price + line total are the same\n // numeric column at numeric(18,4)).\n const amount = format4dp(rate.amount)\n lines.push({\n billingItemId: item.id,\n billingType: 'one_time',\n description: item.description,\n unitPrice: amount,\n quantity: '1',\n lineAmount: amount,\n priceBasis: priceBasisOf(item.priceBasis),\n taxRate: itemTaxRate(item),\n })\n } else if (item.type === 'recurring') {\n const rate = item.rateJson as { unit_price?: number } | null\n if (!rate || typeof rate.unit_price !== 'number') continue\n const unitPrice = format4dp(rate.unit_price)\n lines.push({\n billingItemId: item.id,\n billingType: 'recurring',\n description: item.description,\n unitPrice,\n quantity: '1',\n lineAmount: unitPrice,\n priceBasis: priceBasisOf(item.priceBasis),\n taxRate: itemTaxRate(item),\n })\n }\n }\n return lines\n}\n\n/**\n * Display period of an invoice: min start / max end over the lines' service\n * periods, falling back to `fallback` (the arrears window) when no line\n * carries one (e.g. a one_time-only invoice).\n */\nexport function displayPeriod(lines: readonly InvoiceLineDescriptor[], fallback: BillPeriod): BillPeriod {\n let start: Date | null = null\n let end: Date | null = null\n for (const line of lines) {\n if (line.servicePeriodStart && (!start || line.servicePeriodStart < start)) start = line.servicePeriodStart\n if (line.servicePeriodEnd && (!end || line.servicePeriodEnd > end)) end = line.servicePeriodEnd\n }\n return { periodStart: start ?? fallback.periodStart, periodEnd: end ?? fallback.periodEnd }\n}\n\n/**\n * Spec A: invoice lines for computed recurring service periods (charges and\n * credits). Full-period charges keep the bare item description; partial\n * periods and credits append the covered dates so the line is\n * self-explanatory on any document (language-neutral \u2014 the PDF renders\n * localized labels later).\n */\nexport function buildRecurringLineDescriptors(\n item: Pick<BillingItem, 'id' | 'description' | 'chargeTiming' | 'taxRate' | 'priceBasis'>,\n lines: readonly RecurringLine[],\n): InvoiceLineDescriptor[] {\n return lines.map((line) => {\n const amount = format4dp(line.amount)\n const dated = line.proration !== null || line.kind === 'credit'\n return {\n billingItemId: item.id,\n billingType: 'recurring' as const,\n description: dated\n ? `${item.description} \u00B7 ${isoDate(line.servicePeriodStart)} \u2013 ${isoDate(line.servicePeriodEnd)}`\n : item.description,\n unitPrice: amount,\n quantity: '1',\n lineAmount: amount,\n // Proration and credit lines inherit the item's rate (Spec B) and basis (Spec F).\n priceBasis: priceBasisOf(item.priceBasis),\n taxRate: itemTaxRate(item),\n servicePeriodStart: line.servicePeriodStart,\n servicePeriodEnd: line.servicePeriodEnd,\n metadataExtra: {\n charge_timing: item.chargeTiming,\n ...(line.proration\n ? { proration: { active_days: line.proration.activeDays, period_days: line.proration.periodDays } }\n : {}),\n ...(line.kind === 'credit' ? { credit_for: item.id } : {}),\n },\n }\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function day(value) {
|
|
2
|
+
return value ? value.slice(0, 10) : null;
|
|
3
|
+
}
|
|
4
|
+
function nextDay(iso) {
|
|
5
|
+
const date = /* @__PURE__ */ new Date(`${iso}T00:00:00Z`);
|
|
6
|
+
date.setUTCDate(date.getUTCDate() + 1);
|
|
7
|
+
return date.toISOString().slice(0, 10);
|
|
8
|
+
}
|
|
9
|
+
function itemBillingState(item, today) {
|
|
10
|
+
if (!item.is_active) return { status: "inactive", date: null };
|
|
11
|
+
if (item.type === "usage") return { status: "metered", date: null };
|
|
12
|
+
const start = day(item.bill_start_date) ?? today;
|
|
13
|
+
const billedTo = day(item.billed_to_date);
|
|
14
|
+
const end = day(item.bill_end_date);
|
|
15
|
+
if (item.type === "one_time") {
|
|
16
|
+
if (billedTo) return { status: "billed", date: null };
|
|
17
|
+
return start > today ? { status: "scheduled", date: start } : { status: "awaiting_run", date: null };
|
|
18
|
+
}
|
|
19
|
+
if (!billedTo) return start > today ? { status: "scheduled", date: start } : { status: "awaiting_run", date: null };
|
|
20
|
+
if (end && billedTo >= end) return { status: "fully_billed", date: billedTo };
|
|
21
|
+
if (billedTo >= today) return { status: "current", date: billedTo };
|
|
22
|
+
return { status: "due", date: nextDay(billedTo) };
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
itemBillingState
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=itemBillingState.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/itemBillingState.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Where an item stands against today (account Items tab): has it been billed,\n * and through which date? Pure \u2014 computed from the item row and today's date.\n *\n * inactive the item is switched off\n * metered usage item (billed from usage records, no watermark)\n * scheduled starts after today, nothing billed yet\n * awaiting_run started, but no Bill Run has charged it yet\n * billed one-time item already charged\n * current recurring, charged through today or later (paid ahead)\n * due recurring, charged only through a past date \u2014 the next\n * Bill Run will charge the rest\n * fully_billed recurring with an end date, charged through that end\n */\n\nexport type ItemBillingStatus =\n | 'inactive'\n | 'metered'\n | 'scheduled'\n | 'awaiting_run'\n | 'billed'\n | 'current'\n | 'due'\n | 'fully_billed'\n\nexport type ItemBillingInput = {\n type: 'one_time' | 'recurring' | 'usage'\n is_active: boolean\n bill_start_date: string\n bill_end_date?: string | null\n billed_to_date?: string | null\n}\n\nexport type ItemBillingState = {\n status: ItemBillingStatus\n /** The relevant date for the label: start, billed-through or due-since day (YYYY-MM-DD). */\n date: string | null\n}\n\nfunction day(value: string | null | undefined): string | null {\n return value ? value.slice(0, 10) : null\n}\n\nfunction nextDay(iso: string): string {\n const date = new Date(`${iso}T00:00:00Z`)\n date.setUTCDate(date.getUTCDate() + 1)\n return date.toISOString().slice(0, 10)\n}\n\n/** `today` is a YYYY-MM-DD string (the viewer's calendar day). */\nexport function itemBillingState(item: ItemBillingInput, today: string): ItemBillingState {\n if (!item.is_active) return { status: 'inactive', date: null }\n if (item.type === 'usage') return { status: 'metered', date: null }\n const start = day(item.bill_start_date) ?? today\n const billedTo = day(item.billed_to_date)\n const end = day(item.bill_end_date)\n\n if (item.type === 'one_time') {\n if (billedTo) return { status: 'billed', date: null }\n return start > today ? { status: 'scheduled', date: start } : { status: 'awaiting_run', date: null }\n }\n\n if (!billedTo) return start > today ? { status: 'scheduled', date: start } : { status: 'awaiting_run', date: null }\n if (end && billedTo >= end) return { status: 'fully_billed', date: billedTo }\n if (billedTo >= today) return { status: 'current', date: billedTo }\n return { status: 'due', date: nextDay(billedTo) }\n}\n"],
|
|
5
|
+
"mappings": "AAuCA,SAAS,IAAI,OAAiD;AAC5D,SAAO,QAAQ,MAAM,MAAM,GAAG,EAAE,IAAI;AACtC;AAEA,SAAS,QAAQ,KAAqB;AACpC,QAAM,OAAO,oBAAI,KAAK,GAAG,GAAG,YAAY;AACxC,OAAK,WAAW,KAAK,WAAW,IAAI,CAAC;AACrC,SAAO,KAAK,YAAY,EAAE,MAAM,GAAG,EAAE;AACvC;AAGO,SAAS,iBAAiB,MAAwB,OAAiC;AACxF,MAAI,CAAC,KAAK,UAAW,QAAO,EAAE,QAAQ,YAAY,MAAM,KAAK;AAC7D,MAAI,KAAK,SAAS,QAAS,QAAO,EAAE,QAAQ,WAAW,MAAM,KAAK;AAClE,QAAM,QAAQ,IAAI,KAAK,eAAe,KAAK;AAC3C,QAAM,WAAW,IAAI,KAAK,cAAc;AACxC,QAAM,MAAM,IAAI,KAAK,aAAa;AAElC,MAAI,KAAK,SAAS,YAAY;AAC5B,QAAI,SAAU,QAAO,EAAE,QAAQ,UAAU,MAAM,KAAK;AACpD,WAAO,QAAQ,QAAQ,EAAE,QAAQ,aAAa,MAAM,MAAM,IAAI,EAAE,QAAQ,gBAAgB,MAAM,KAAK;AAAA,EACrG;AAEA,MAAI,CAAC,SAAU,QAAO,QAAQ,QAAQ,EAAE,QAAQ,aAAa,MAAM,MAAM,IAAI,EAAE,QAAQ,gBAAgB,MAAM,KAAK;AAClH,MAAI,OAAO,YAAY,IAAK,QAAO,EAAE,QAAQ,gBAAgB,MAAM,SAAS;AAC5E,MAAI,YAAY,MAAO,QAAO,EAAE,QAAQ,WAAW,MAAM,SAAS;AAClE,SAAO,EAAE,QAAQ,OAAO,MAAM,QAAQ,QAAQ,EAAE;AAClD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { toIsoDate, toUtcMidnight } from "./dateOnly.js";
|
|
2
|
+
function planPriceChange(item, effectiveDate) {
|
|
3
|
+
if (item.type !== "recurring") return { ok: false, error: "billing.item.price_change_recurring_only" };
|
|
4
|
+
if (!item.isActive || item.deletedAt) return { ok: false, error: "billing.item.price_change_inactive" };
|
|
5
|
+
const effective = toUtcMidnight(effectiveDate);
|
|
6
|
+
const start = toUtcMidnight(item.billStartDate);
|
|
7
|
+
if (effective.getTime() <= start.getTime()) return { ok: false, error: "billing.item.price_change_before_start" };
|
|
8
|
+
const end = item.billEndDate ? toUtcMidnight(item.billEndDate) : null;
|
|
9
|
+
if (end && effective.getTime() > end.getTime()) return { ok: false, error: "billing.item.price_change_after_end" };
|
|
10
|
+
const oldItemEndDate = new Date(effective);
|
|
11
|
+
oldItemEndDate.setUTCDate(oldItemEndDate.getUTCDate() - 1);
|
|
12
|
+
return { ok: true, plan: { oldItemEndDate, newItemStartDate: effective, newItemEndDate: end } };
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
planPriceChange,
|
|
16
|
+
toIsoDate
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=itemPriceChange.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/itemPriceChange.ts"],
|
|
4
|
+
"sourcesContent": ["import { toIsoDate, toUtcMidnight } from './dateOnly'\n\n/**\n * Change the price of a recurring item from a date on (Spec F follow-up):\n * the current item ends the day before `effectiveDate`, and a copy with the\n * new price starts on `effectiveDate` and keeps the original end date.\n *\n * Nothing is billed twice: if the old item was already billed past the\n * change (in advance), the engine credits the unused days (prorate on) and\n * the new item charges them at the new price; with prorate off the old item\n * keeps them and the new item starts charging at its next full period.\n *\n * Pure \u2014 validates and computes the dates; the command does the writes.\n */\n\nexport type PriceChangeItem = {\n type: string\n isActive: boolean\n deletedAt?: Date | string | null\n billStartDate: Date | string\n billEndDate?: Date | string | null\n}\n\nexport type PriceChangePlan = {\n /** New end date of the current item: the day before the change. */\n oldItemEndDate: Date\n /** Start of the new item: the effective date. */\n newItemStartDate: Date\n /** The new item keeps the original end date (null = open-ended). */\n newItemEndDate: Date | null\n}\n\nexport type PriceChangeError =\n | 'billing.item.price_change_recurring_only'\n | 'billing.item.price_change_inactive'\n | 'billing.item.price_change_before_start'\n | 'billing.item.price_change_after_end'\n\nexport function planPriceChange(\n item: PriceChangeItem,\n effectiveDate: Date | string,\n): { ok: true; plan: PriceChangePlan } | { ok: false; error: PriceChangeError } {\n if (item.type !== 'recurring') return { ok: false, error: 'billing.item.price_change_recurring_only' }\n if (!item.isActive || item.deletedAt) return { ok: false, error: 'billing.item.price_change_inactive' }\n const effective = toUtcMidnight(effectiveDate)\n const start = toUtcMidnight(item.billStartDate)\n // The current item must keep at least one day.\n if (effective.getTime() <= start.getTime()) return { ok: false, error: 'billing.item.price_change_before_start' }\n const end = item.billEndDate ? toUtcMidnight(item.billEndDate) : null\n if (end && effective.getTime() > end.getTime()) return { ok: false, error: 'billing.item.price_change_after_end' }\n const oldItemEndDate = new Date(effective)\n oldItemEndDate.setUTCDate(oldItemEndDate.getUTCDate() - 1)\n return { ok: true, plan: { oldItemEndDate, newItemStartDate: effective, newItemEndDate: end } }\n}\n\nexport { toIsoDate }\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,WAAW,qBAAqB;AAsClC,SAAS,gBACd,MACA,eAC8E;AAC9E,MAAI,KAAK,SAAS,YAAa,QAAO,EAAE,IAAI,OAAO,OAAO,2CAA2C;AACrG,MAAI,CAAC,KAAK,YAAY,KAAK,UAAW,QAAO,EAAE,IAAI,OAAO,OAAO,qCAAqC;AACtG,QAAM,YAAY,cAAc,aAAa;AAC7C,QAAM,QAAQ,cAAc,KAAK,aAAa;AAE9C,MAAI,UAAU,QAAQ,KAAK,MAAM,QAAQ,EAAG,QAAO,EAAE,IAAI,OAAO,OAAO,yCAAyC;AAChH,QAAM,MAAM,KAAK,cAAc,cAAc,KAAK,WAAW,IAAI;AACjE,MAAI,OAAO,UAAU,QAAQ,IAAI,IAAI,QAAQ,EAAG,QAAO,EAAE,IAAI,OAAO,OAAO,sCAAsC;AACjH,QAAM,iBAAiB,IAAI,KAAK,SAAS;AACzC,iBAAe,WAAW,eAAe,WAAW,IAAI,CAAC;AACzD,SAAO,EAAE,IAAI,MAAM,MAAM,EAAE,gBAAgB,kBAAkB,WAAW,gBAAgB,IAAI,EAAE;AAChG;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|