@dainamite/billing 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/dist/modules/billing/acl.js +13 -1
- package/dist/modules/billing/acl.js.map +2 -2
- package/dist/modules/billing/api/accounts/[id]/summary/route.js +52 -0
- package/dist/modules/billing/api/accounts/[id]/summary/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +47 -4
- package/dist/modules/billing/api/accounts/route.js.map +2 -2
- package/dist/modules/billing/api/export/account/[id]/route.js +11 -7
- package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/[id]/memos/route.js +54 -0
- package/dist/modules/billing/api/invoices/[id]/memos/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/pdf/route.js +70 -0
- package/dist/modules/billing/api/invoices/[id]/pdf/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/route.js +44 -24
- package/dist/modules/billing/api/invoices/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/[id]/send/route.js +107 -0
- package/dist/modules/billing/api/invoices/[id]/send/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/add-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/add-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/delete-draft/route.js +33 -0
- package/dist/modules/billing/api/invoices/delete-draft/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/edit-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/post/route.js +2 -2
- package/dist/modules/billing/api/invoices/post/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/remove-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/remove-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/route.js +45 -46
- package/dist/modules/billing/api/invoices/route.js.map +2 -2
- package/dist/modules/billing/api/items/bulk/route.js +1 -1
- package/dist/modules/billing/api/items/bulk/route.js.map +2 -2
- package/dist/modules/billing/api/items/change-price/route.js +44 -0
- package/dist/modules/billing/api/items/change-price/route.js.map +7 -0
- package/dist/modules/billing/api/items/route.js +19 -7
- package/dist/modules/billing/api/items/route.js.map +2 -2
- package/dist/modules/billing/api/payment-imports/[id]/route.js +49 -0
- package/dist/modules/billing/api/payment-imports/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/payment-imports/route.js +57 -0
- package/dist/modules/billing/api/payment-imports/route.js.map +7 -0
- package/dist/modules/billing/api/payments/[id]/route.js +60 -0
- package/dist/modules/billing/api/payments/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/payments/allocate/route.js +35 -0
- package/dist/modules/billing/api/payments/allocate/route.js.map +7 -0
- package/dist/modules/billing/api/payments/assign/route.js +36 -0
- package/dist/modules/billing/api/payments/assign/route.js.map +7 -0
- package/dist/modules/billing/api/payments/convert-to-topup/route.js +35 -0
- package/dist/modules/billing/api/payments/convert-to-topup/route.js.map +7 -0
- package/dist/modules/billing/api/payments/ignore/route.js +35 -0
- package/dist/modules/billing/api/payments/ignore/route.js.map +7 -0
- package/dist/modules/billing/api/payments/import/route.js +63 -0
- package/dist/modules/billing/api/payments/import/route.js.map +7 -0
- package/dist/modules/billing/api/payments/restore/route.js +35 -0
- package/dist/modules/billing/api/payments/restore/route.js.map +7 -0
- package/dist/modules/billing/api/payments/route.js +114 -0
- package/dist/modules/billing/api/payments/route.js.map +7 -0
- package/dist/modules/billing/api/payments/unallocate/route.js +35 -0
- package/dist/modules/billing/api/payments/unallocate/route.js.map +7 -0
- package/dist/modules/billing/api/payments/unassign/route.js +35 -0
- package/dist/modules/billing/api/payments/unassign/route.js.map +7 -0
- package/dist/modules/billing/api/run-outcomes/route.js +43 -6
- package/dist/modules/billing/api/run-outcomes/route.js.map +2 -2
- package/dist/modules/billing/api/runs/[id]/post-drafts/route.js +61 -0
- package/dist/modules/billing/api/runs/[id]/post-drafts/route.js.map +7 -0
- package/dist/modules/billing/api/runs/[id]/route.js +53 -0
- package/dist/modules/billing/api/runs/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js +1 -1
- package/dist/modules/billing/api/runs/retry-failed/route.js.map +2 -2
- package/dist/modules/billing/api/runs/route.js +9 -3
- package/dist/modules/billing/api/runs/route.js.map +2 -2
- package/dist/modules/billing/api/settings/number-preview/route.js +42 -0
- package/dist/modules/billing/api/settings/number-preview/route.js.map +7 -0
- package/dist/modules/billing/api/settings/route.js +92 -0
- package/dist/modules/billing/api/settings/route.js.map +7 -0
- package/dist/modules/billing/api/statements/route.js +3 -0
- package/dist/modules/billing/api/statements/route.js.map +2 -2
- package/dist/modules/billing/api/test-invoices/wipe/route.js +2 -2
- package/dist/modules/billing/api/test-invoices/wipe/route.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +121 -232
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/create/page.js +6 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/page.js +63 -4
- package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js +297 -65
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +3 -3
- package/dist/modules/billing/backend/billing/invoices/page.js +6 -172
- package/dist/modules/billing/backend/billing/invoices/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/[id]/page.js +51 -7
- package/dist/modules/billing/backend/billing/items/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/create/page.js +9 -6
- package/dist/modules/billing/backend/billing/items/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/page.js +8 -2
- package/dist/modules/billing/backend/billing/items/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/payments/[id]/page.js +297 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js +74 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/page.js +58 -0
- package/dist/modules/billing/backend/billing/payments/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/payments/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js +49 -14
- package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/runs/page.js +58 -7
- package/dist/modules/billing/backend/billing/runs/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/[id]/page.js +61 -6
- package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/page.js +1 -1
- package/dist/modules/billing/backend/billing/statements/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/topups/page.js +1 -1
- package/dist/modules/billing/backend/billing/topups/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/transactions/page.js +1 -1
- package/dist/modules/billing/backend/billing/transactions/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/usage/create/page.js +3 -2
- package/dist/modules/billing/backend/billing/usage/create/page.js.map +2 -2
- package/dist/modules/billing/backend/config/billing/page.js +472 -0
- package/dist/modules/billing/backend/config/billing/page.js.map +7 -0
- package/dist/modules/billing/backend/config/billing/page.meta.js +19 -0
- package/dist/modules/billing/backend/config/billing/page.meta.js.map +7 -0
- package/dist/modules/billing/cli.js +22 -1
- package/dist/modules/billing/cli.js.map +2 -2
- package/dist/modules/billing/commands/accounts.js +6 -0
- package/dist/modules/billing/commands/accounts.js.map +2 -2
- package/dist/modules/billing/commands/index.js +3 -0
- package/dist/modules/billing/commands/index.js.map +2 -2
- package/dist/modules/billing/commands/invoices.js +269 -167
- package/dist/modules/billing/commands/invoices.js.map +2 -2
- package/dist/modules/billing/commands/items.js +108 -0
- package/dist/modules/billing/commands/items.js.map +2 -2
- package/dist/modules/billing/commands/memos.js +179 -0
- package/dist/modules/billing/commands/memos.js.map +7 -0
- package/dist/modules/billing/commands/payments.js +379 -0
- package/dist/modules/billing/commands/payments.js.map +7 -0
- package/dist/modules/billing/commands/runs.js +151 -4
- package/dist/modules/billing/commands/runs.js.map +2 -2
- package/dist/modules/billing/commands/settings.js +59 -0
- package/dist/modules/billing/commands/settings.js.map +7 -0
- package/dist/modules/billing/commands/topups.js +9 -2
- package/dist/modules/billing/commands/topups.js.map +2 -2
- package/dist/modules/billing/commands/transactions.js +2 -2
- package/dist/modules/billing/commands/transactions.js.map +2 -2
- package/dist/modules/billing/components/AccountForm.js +149 -42
- package/dist/modules/billing/components/AccountForm.js.map +2 -2
- package/dist/modules/billing/components/AccountInvoicesSection.js +64 -0
- package/dist/modules/billing/components/AccountInvoicesSection.js.map +7 -0
- package/dist/modules/billing/components/AccountPaymentsSection.js +43 -0
- package/dist/modules/billing/components/AccountPaymentsSection.js.map +7 -0
- package/dist/modules/billing/components/AccountPicker.js +56 -0
- package/dist/modules/billing/components/AccountPicker.js.map +7 -0
- package/dist/modules/billing/components/AllocatePaymentDialog.js +134 -0
- package/dist/modules/billing/components/AllocatePaymentDialog.js.map +7 -0
- package/dist/modules/billing/components/BillingDataTable.js +21 -0
- package/dist/modules/billing/components/BillingDataTable.js.map +7 -0
- package/dist/modules/billing/components/BillingSettingsLink.js +19 -0
- package/dist/modules/billing/components/BillingSettingsLink.js.map +7 -0
- package/dist/modules/billing/components/ChangeItemPriceDialog.js +158 -0
- package/dist/modules/billing/components/ChangeItemPriceDialog.js.map +7 -0
- package/dist/modules/billing/components/CreateMemoDialog.js +200 -0
- package/dist/modules/billing/components/CreateMemoDialog.js.map +7 -0
- package/dist/modules/billing/components/CustomerLabel.js +33 -0
- package/dist/modules/billing/components/CustomerLabel.js.map +7 -0
- package/dist/modules/billing/components/InvoicesTable.js +270 -0
- package/dist/modules/billing/components/InvoicesTable.js.map +7 -0
- package/dist/modules/billing/components/ItemForm.js +176 -44
- package/dist/modules/billing/components/ItemForm.js.map +2 -2
- package/dist/modules/billing/components/ItemPriceField.js +46 -0
- package/dist/modules/billing/components/ItemPriceField.js.map +7 -0
- package/dist/modules/billing/components/LineFormDialog.js +25 -2
- package/dist/modules/billing/components/LineFormDialog.js.map +2 -2
- package/dist/modules/billing/components/ManualPaymentDialog.js +122 -0
- package/dist/modules/billing/components/ManualPaymentDialog.js.map +7 -0
- package/dist/modules/billing/components/MoneyInput.js +14 -0
- package/dist/modules/billing/components/MoneyInput.js.map +7 -0
- package/dist/modules/billing/components/PaymentUploadDialog.js +115 -0
- package/dist/modules/billing/components/PaymentUploadDialog.js.map +7 -0
- package/dist/modules/billing/components/PaymentsTable.js +171 -0
- package/dist/modules/billing/components/PaymentsTable.js.map +7 -0
- package/dist/modules/billing/components/PostRunDraftsButton.js +105 -0
- package/dist/modules/billing/components/PostRunDraftsButton.js.map +7 -0
- package/dist/modules/billing/components/PrepaidPanel.js +14 -4
- package/dist/modules/billing/components/PrepaidPanel.js.map +2 -2
- package/dist/modules/billing/components/RunCyclesSection.js +209 -0
- package/dist/modules/billing/components/RunCyclesSection.js.map +7 -0
- package/dist/modules/billing/components/TestRunActions.js +127 -0
- package/dist/modules/billing/components/TestRunActions.js.map +7 -0
- package/dist/modules/billing/components/account/AccountItemsTab.js +157 -0
- package/dist/modules/billing/components/account/AccountItemsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountRunBillingDialog.js +122 -0
- package/dist/modules/billing/components/account/AccountRunBillingDialog.js.map +7 -0
- package/dist/modules/billing/components/account/AccountRunsTab.js +152 -0
- package/dist/modules/billing/components/account/AccountRunsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountStatementsTab.js +98 -0
- package/dist/modules/billing/components/account/AccountStatementsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountSummaryCards.js +125 -0
- package/dist/modules/billing/components/account/AccountSummaryCards.js.map +7 -0
- package/dist/modules/billing/components/account/AccountUsageTab.js +91 -0
- package/dist/modules/billing/components/account/AccountUsageTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountView.js +73 -0
- package/dist/modules/billing/components/account/AccountView.js.map +7 -0
- package/dist/modules/billing/components/account/accountRow.js +39 -0
- package/dist/modules/billing/components/account/accountRow.js.map +7 -0
- package/dist/modules/billing/components/customerNames.js +42 -0
- package/dist/modules/billing/components/customerNames.js.map +7 -0
- package/dist/modules/billing/data/entities.js +560 -4
- package/dist/modules/billing/data/entities.js.map +2 -2
- package/dist/modules/billing/data/entityIds.js +10 -1
- package/dist/modules/billing/data/entityIds.js.map +2 -2
- package/dist/modules/billing/data/validators.js +269 -15
- package/dist/modules/billing/data/validators.js.map +2 -2
- package/dist/modules/billing/events.js +50 -0
- package/dist/modules/billing/events.js.map +2 -2
- package/dist/modules/billing/i18n/de.json +880 -0
- package/dist/modules/billing/i18n/en.json +457 -21
- package/dist/modules/billing/i18n/es.json +880 -0
- package/dist/modules/billing/i18n/pl.json +458 -22
- package/dist/modules/billing/index.js +6 -2
- package/dist/modules/billing/index.js.map +2 -2
- package/dist/modules/billing/lib/accountFinancials.js +58 -0
- package/dist/modules/billing/lib/accountFinancials.js.map +7 -0
- package/dist/modules/billing/lib/accountLock.js +14 -0
- package/dist/modules/billing/lib/accountLock.js.map +7 -0
- package/dist/modules/billing/lib/accountStatus.js +9 -7
- package/dist/modules/billing/lib/accountStatus.js.map +2 -2
- package/dist/modules/billing/lib/accountSummary.js +106 -0
- package/dist/modules/billing/lib/accountSummary.js.map +7 -0
- package/dist/modules/billing/lib/allocation.js +356 -0
- package/dist/modules/billing/lib/allocation.js.map +7 -0
- package/dist/modules/billing/lib/allocationEvents.js +22 -0
- package/dist/modules/billing/lib/allocationEvents.js.map +7 -0
- package/dist/modules/billing/lib/billPeriod.js +15 -1
- package/dist/modules/billing/lib/billPeriod.js.map +2 -2
- package/dist/modules/billing/lib/billRunEngine.js +237 -102
- package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
- package/dist/modules/billing/lib/billingSettings.js +116 -0
- package/dist/modules/billing/lib/billingSettings.js.map +7 -0
- package/dist/modules/billing/lib/commandRoute.js +81 -0
- package/dist/modules/billing/lib/commandRoute.js.map +7 -0
- package/dist/modules/billing/lib/customerLookup.js +97 -0
- package/dist/modules/billing/lib/customerLookup.js.map +7 -0
- package/dist/modules/billing/lib/dateOnly.js +13 -0
- package/dist/modules/billing/lib/dateOnly.js.map +7 -0
- package/dist/modules/billing/lib/documentParties.js +16 -0
- package/dist/modules/billing/lib/documentParties.js.map +7 -0
- package/dist/modules/billing/lib/documentTypes.js +27 -0
- package/dist/modules/billing/lib/documentTypes.js.map +7 -0
- package/dist/modules/billing/lib/dueDate.js +18 -0
- package/dist/modules/billing/lib/dueDate.js.map +7 -0
- package/dist/modules/billing/lib/iban.js +29 -0
- package/dist/modules/billing/lib/iban.js.map +7 -0
- package/dist/modules/billing/lib/invoiceDelivery.js +133 -0
- package/dist/modules/billing/lib/invoiceDelivery.js.map +7 -0
- package/dist/modules/billing/lib/invoiceLanguages.js +28 -0
- package/dist/modules/billing/lib/invoiceLanguages.js.map +7 -0
- package/dist/modules/billing/lib/invoiceWriter.js +107 -99
- package/dist/modules/billing/lib/invoiceWriter.js.map +2 -2
- package/dist/modules/billing/lib/itemBillingState.js +27 -0
- package/dist/modules/billing/lib/itemBillingState.js.map +7 -0
- package/dist/modules/billing/lib/itemPriceChange.js +18 -0
- package/dist/modules/billing/lib/itemPriceChange.js.map +7 -0
- package/dist/modules/billing/lib/itemSelector.js.map +2 -2
- package/dist/modules/billing/lib/lineAmounts.js +32 -0
- package/dist/modules/billing/lib/lineAmounts.js.map +7 -0
- package/dist/modules/billing/lib/memoGuards.js +25 -0
- package/dist/modules/billing/lib/memoGuards.js.map +7 -0
- package/dist/modules/billing/lib/navigation.js +48 -0
- package/dist/modules/billing/lib/navigation.js.map +7 -0
- package/dist/modules/billing/lib/numbering.js +120 -0
- package/dist/modules/billing/lib/numbering.js.map +7 -0
- package/dist/modules/billing/lib/overdue.js +36 -0
- package/dist/modules/billing/lib/overdue.js.map +7 -0
- package/dist/modules/billing/lib/paymentCsv.js +125 -0
- package/dist/modules/billing/lib/paymentCsv.js.map +7 -0
- package/dist/modules/billing/lib/paymentGateway.js +15 -0
- package/dist/modules/billing/lib/paymentGateway.js.map +7 -0
- package/dist/modules/billing/lib/paymentImport.js +164 -0
- package/dist/modules/billing/lib/paymentImport.js.map +7 -0
- package/dist/modules/billing/lib/paymentMatcher.js +36 -0
- package/dist/modules/billing/lib/paymentMatcher.js.map +7 -0
- package/dist/modules/billing/lib/pdf/attachmentStore.js +46 -0
- package/dist/modules/billing/lib/pdf/attachmentStore.js.map +7 -0
- package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js +7 -0
- package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoiceModel.js +194 -0
- package/dist/modules/billing/lib/pdf/invoiceModel.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoicePdf.js +165 -0
- package/dist/modules/billing/lib/pdf/invoicePdf.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoiceTemplate.js +276 -0
- package/dist/modules/billing/lib/pdf/invoiceTemplate.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConfig.js +17 -25
- package/dist/modules/billing/lib/prepaidConfig.js.map +2 -2
- package/dist/modules/billing/lib/prepaidConsume.js +26 -7
- package/dist/modules/billing/lib/prepaidConsume.js.map +2 -2
- package/dist/modules/billing/lib/prepaidPeriodClose.js +277 -158
- package/dist/modules/billing/lib/prepaidPeriodClose.js.map +2 -2
- package/dist/modules/billing/lib/prepaidTaxPoint.js +37 -0
- package/dist/modules/billing/lib/prepaidTaxPoint.js.map +7 -0
- package/dist/modules/billing/lib/seeds/demo.js +51 -3
- package/dist/modules/billing/lib/seeds/demo.js.map +2 -2
- package/dist/modules/billing/lib/seeds/paymentsSample.js +93 -0
- package/dist/modules/billing/lib/seeds/paymentsSample.js.map +7 -0
- package/dist/modules/billing/lib/seeds/sellerSettings.js +41 -0
- package/dist/modules/billing/lib/seeds/sellerSettings.js.map +7 -0
- package/dist/modules/billing/lib/servicePeriods.js +121 -0
- package/dist/modules/billing/lib/servicePeriods.js.map +7 -0
- package/dist/modules/billing/lib/testRunCleanup.js +24 -0
- package/dist/modules/billing/lib/testRunCleanup.js.map +7 -0
- package/dist/modules/billing/lib/topupCapture.js +2 -2
- package/dist/modules/billing/lib/topupCapture.js.map +2 -2
- package/dist/modules/billing/lib/topupReceipt.js +58 -79
- package/dist/modules/billing/lib/topupReceipt.js.map +2 -2
- package/dist/modules/billing/lib/usageRater.js.map +1 -1
- package/dist/modules/billing/lib/usageRunner.js +14 -6
- package/dist/modules/billing/lib/usageRunner.js.map +2 -2
- package/dist/modules/billing/lib/vat.js +39 -0
- package/dist/modules/billing/lib/vat.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js +15 -0
- package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js +25 -0
- package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js +115 -0
- package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js +47 -0
- package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js +89 -0
- package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js +17 -0
- package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js +27 -0
- package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js +29 -0
- package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js.map +7 -0
- package/dist/modules/billing/setup.js +7 -184
- package/dist/modules/billing/setup.js.map +2 -2
- package/dist/modules/billing/subscribers/credit-memo-auto-send.js +16 -0
- package/dist/modules/billing/subscribers/credit-memo-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/debit-memo-apply-credit.js +16 -0
- package/dist/modules/billing/subscribers/debit-memo-apply-credit.js.map +7 -0
- package/dist/modules/billing/subscribers/debit-memo-auto-send.js +16 -0
- package/dist/modules/billing/subscribers/debit-memo-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/invoice-apply-credit.js +34 -0
- package/dist/modules/billing/subscribers/invoice-apply-credit.js.map +7 -0
- package/dist/modules/billing/subscribers/invoice-auto-send.js +30 -0
- package/dist/modules/billing/subscribers/invoice-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js +69 -33
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +2 -2
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js +4 -6
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +2 -2
- package/generated/entities/billing_account/index.ts +3 -0
- package/generated/entities/billing_allocation/index.ts +18 -0
- package/generated/entities/billing_document_sequence/index.ts +11 -0
- package/generated/entities/billing_invoice/index.ts +38 -0
- package/generated/entities/billing_invoice_line/index.ts +24 -0
- package/generated/entities/billing_item/index.ts +4 -0
- package/generated/entities/billing_payment/index.ts +25 -0
- package/generated/entities/billing_payment_import/index.ts +14 -0
- package/generated/entities/billing_run/index.ts +0 -1
- package/generated/entities/billing_run_outcome/index.ts +1 -0
- package/generated/entities/billing_settings/index.ts +26 -0
- package/generated/entities/billing_statement/index.ts +1 -0
- package/generated/entities.ids.generated.ts +7 -0
- package/package.json +5 -1
- package/src/modules/billing/__integration__/TC-BILL-004.spec.ts +171 -0
- package/src/modules/billing/__integration__/TC-BILL-005.spec.ts +134 -0
- package/src/modules/billing/__integration__/TC-BILL-006.spec.ts +101 -0
- package/src/modules/billing/__integration__/TC-BILL-007.spec.ts +140 -0
- package/src/modules/billing/__integration__/TC-BILL-008.spec.ts +108 -0
- package/src/modules/billing/__integration__/TC-BILL-009.spec.ts +113 -0
- package/src/modules/billing/__integration__/TC-BILL-010.spec.ts +118 -0
- package/src/modules/billing/__integration__/TC-BILL-011.spec.ts +142 -0
- package/src/modules/billing/__integration__/TC-BILL-012.spec.ts +115 -0
- package/src/modules/billing/__integration__/TC-BILL-013.spec.ts +118 -0
- package/src/modules/billing/__integration__/sellerSettings.ts +24 -0
- package/src/modules/billing/__tests__/commands/invoices.test.ts +436 -354
- package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +33 -0
- package/src/modules/billing/__tests__/commands/memos.test.ts +205 -0
- package/src/modules/billing/__tests__/commands/payments.test.ts +241 -0
- package/src/modules/billing/__tests__/commands/runsDelete.test.ts +146 -0
- package/src/modules/billing/__tests__/commands/runsPostDrafts.test.ts +106 -0
- package/src/modules/billing/__tests__/commands/topups.test.ts +11 -0
- package/src/modules/billing/__tests__/entityRegistry.test.ts +8 -1
- package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +28 -11
- package/src/modules/billing/__tests__/i18n/dictionaries.test.ts +64 -0
- package/src/modules/billing/__tests__/lib/accountFinancials.test.ts +53 -0
- package/src/modules/billing/__tests__/lib/accountSummary.test.ts +97 -0
- package/src/modules/billing/__tests__/lib/allocation.test.ts +319 -0
- package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +228 -17
- package/src/modules/billing/__tests__/lib/billingSettings.test.ts +109 -0
- package/src/modules/billing/__tests__/lib/customerLookup.test.ts +156 -0
- package/src/modules/billing/__tests__/lib/dueDate.test.ts +35 -0
- package/src/modules/billing/__tests__/lib/iban.test.ts +38 -0
- package/src/modules/billing/__tests__/lib/invoiceDelivery.test.ts +206 -0
- package/src/modules/billing/__tests__/lib/invoiceLanguages.test.ts +49 -0
- package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +212 -10
- package/src/modules/billing/__tests__/lib/itemBillingState.test.ts +42 -0
- package/src/modules/billing/__tests__/lib/itemPriceChange.test.ts +165 -0
- package/src/modules/billing/__tests__/lib/lineAmounts.test.ts +86 -0
- package/src/modules/billing/__tests__/lib/navigation.test.ts +71 -0
- package/src/modules/billing/__tests__/lib/numbering.test.ts +91 -0
- package/src/modules/billing/__tests__/lib/overdue.test.ts +36 -0
- package/src/modules/billing/__tests__/lib/paymentCsv.test.ts +148 -0
- package/src/modules/billing/__tests__/lib/paymentImport.test.ts +113 -0
- package/src/modules/billing/__tests__/lib/paymentsSample.test.ts +73 -0
- package/src/modules/billing/__tests__/lib/pdf/invoicePdf.test.ts +269 -0
- package/src/modules/billing/__tests__/lib/pdf/invoicePdfService.test.ts +147 -0
- package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +374 -98
- package/src/modules/billing/__tests__/lib/prepaidTaxPoint.test.ts +118 -0
- package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +53 -2
- package/src/modules/billing/__tests__/lib/sellerSettings.test.ts +28 -0
- package/src/modules/billing/__tests__/lib/servicePeriods.test.ts +190 -0
- package/src/modules/billing/__tests__/lib/topupCapture.test.ts +6 -28
- package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +21 -52
- package/src/modules/billing/__tests__/lib/vat.test.ts +51 -0
- package/src/modules/billing/__tests__/package-isolation.test.ts +22 -8
- package/src/modules/billing/__tests__/setup.test.ts +13 -4
- package/src/modules/billing/__tests__/validators.test.ts +197 -3
- package/src/modules/billing/acl.ts +12 -0
- package/src/modules/billing/api/accounts/[id]/summary/route.ts +62 -0
- package/src/modules/billing/api/accounts/route.ts +59 -4
- package/src/modules/billing/api/export/account/[id]/route.ts +13 -11
- package/src/modules/billing/api/invoices/[id]/memos/route.ts +63 -0
- package/src/modules/billing/api/invoices/[id]/pdf/route.ts +80 -0
- package/src/modules/billing/api/invoices/[id]/route.ts +57 -70
- package/src/modules/billing/api/invoices/[id]/send/route.ts +127 -0
- package/src/modules/billing/api/invoices/add-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/delete-draft/route.ts +33 -0
- package/src/modules/billing/api/invoices/edit-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/post/route.ts +7 -6
- package/src/modules/billing/api/invoices/remove-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/route.ts +81 -86
- package/src/modules/billing/api/items/bulk/route.ts +2 -1
- package/src/modules/billing/api/items/change-price/route.ts +54 -0
- package/src/modules/billing/api/items/route.ts +23 -7
- package/src/modules/billing/api/payment-imports/[id]/route.ts +49 -0
- package/src/modules/billing/api/payment-imports/route.ts +57 -0
- package/src/modules/billing/api/payments/[id]/route.ts +63 -0
- package/src/modules/billing/api/payments/allocate/route.ts +35 -0
- package/src/modules/billing/api/payments/assign/route.ts +36 -0
- package/src/modules/billing/api/payments/convert-to-topup/route.ts +35 -0
- package/src/modules/billing/api/payments/ignore/route.ts +35 -0
- package/src/modules/billing/api/payments/import/route.ts +72 -0
- package/src/modules/billing/api/payments/restore/route.ts +35 -0
- package/src/modules/billing/api/payments/route.ts +147 -0
- package/src/modules/billing/api/payments/unallocate/route.ts +35 -0
- package/src/modules/billing/api/payments/unassign/route.ts +35 -0
- package/src/modules/billing/api/run-outcomes/route.ts +49 -6
- package/src/modules/billing/api/runs/[id]/post-drafts/route.ts +71 -0
- package/src/modules/billing/api/runs/[id]/route.ts +59 -0
- package/src/modules/billing/api/runs/retry-failed/route.ts +2 -1
- package/src/modules/billing/api/runs/route.ts +15 -7
- package/src/modules/billing/api/settings/number-preview/route.ts +46 -0
- package/src/modules/billing/api/settings/route.ts +116 -0
- package/src/modules/billing/api/statements/route.ts +5 -0
- package/src/modules/billing/api/test-invoices/wipe/route.ts +2 -2
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +151 -372
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +6 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +70 -4
- package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +444 -95
- package/src/modules/billing/backend/billing/invoices/page.tsx +8 -227
- package/src/modules/billing/backend/billing/items/[id]/page.tsx +75 -6
- package/src/modules/billing/backend/billing/items/create/page.tsx +13 -8
- package/src/modules/billing/backend/billing/items/page.tsx +18 -2
- package/src/modules/billing/backend/billing/payments/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/payments/[id]/page.tsx +415 -0
- package/src/modules/billing/backend/billing/payments/imports/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/payments/imports/[id]/page.tsx +106 -0
- package/src/modules/billing/backend/billing/payments/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/payments/page.tsx +56 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.tsx +49 -18
- package/src/modules/billing/backend/billing/runs/page.tsx +59 -8
- package/src/modules/billing/backend/billing/statements/[id]/page.tsx +86 -5
- package/src/modules/billing/backend/billing/statements/page.tsx +1 -1
- package/src/modules/billing/backend/billing/topups/page.tsx +1 -1
- package/src/modules/billing/backend/billing/transactions/page.tsx +1 -1
- package/src/modules/billing/backend/billing/usage/create/page.tsx +3 -2
- package/src/modules/billing/backend/config/billing/page.meta.ts +21 -0
- package/src/modules/billing/backend/config/billing/page.tsx +549 -0
- package/src/modules/billing/cli.ts +31 -1
- package/src/modules/billing/commands/accounts.ts +6 -0
- package/src/modules/billing/commands/index.ts +3 -0
- package/src/modules/billing/commands/invoices.ts +364 -231
- package/src/modules/billing/commands/items.ts +143 -0
- package/src/modules/billing/commands/memos.ts +218 -0
- package/src/modules/billing/commands/payments.ts +428 -0
- package/src/modules/billing/commands/runs.ts +205 -3
- package/src/modules/billing/commands/settings.ts +77 -0
- package/src/modules/billing/commands/topups.ts +12 -3
- package/src/modules/billing/commands/transactions.ts +2 -2
- package/src/modules/billing/components/AccountForm.tsx +199 -53
- package/src/modules/billing/components/AccountInvoicesSection.tsx +75 -0
- package/src/modules/billing/components/AccountPaymentsSection.tsx +42 -0
- package/src/modules/billing/components/AccountPicker.tsx +70 -0
- package/src/modules/billing/components/AllocatePaymentDialog.tsx +167 -0
- package/src/modules/billing/components/BillingDataTable.tsx +34 -0
- package/src/modules/billing/components/BillingSettingsLink.tsx +25 -0
- package/src/modules/billing/components/ChangeItemPriceDialog.tsx +180 -0
- package/src/modules/billing/components/CreateMemoDialog.tsx +232 -0
- package/src/modules/billing/components/CustomerLabel.tsx +53 -0
- package/src/modules/billing/components/InvoicesTable.tsx +344 -0
- package/src/modules/billing/components/ItemForm.tsx +233 -44
- package/src/modules/billing/components/ItemPriceField.tsx +81 -0
- package/src/modules/billing/components/LineFormDialog.tsx +25 -1
- package/src/modules/billing/components/ManualPaymentDialog.tsx +140 -0
- package/src/modules/billing/components/MoneyInput.tsx +26 -0
- package/src/modules/billing/components/PaymentUploadDialog.tsx +148 -0
- package/src/modules/billing/components/PaymentsTable.tsx +213 -0
- package/src/modules/billing/components/PostRunDraftsButton.tsx +129 -0
- package/src/modules/billing/components/PrepaidPanel.tsx +19 -6
- package/src/modules/billing/components/RunCyclesSection.tsx +298 -0
- package/src/modules/billing/components/TestRunActions.tsx +152 -0
- package/src/modules/billing/components/account/AccountItemsTab.tsx +192 -0
- package/src/modules/billing/components/account/AccountRunBillingDialog.tsx +147 -0
- package/src/modules/billing/components/account/AccountRunsTab.tsx +201 -0
- package/src/modules/billing/components/account/AccountStatementsTab.tsx +120 -0
- package/src/modules/billing/components/account/AccountSummaryCards.tsx +153 -0
- package/src/modules/billing/components/account/AccountUsageTab.tsx +122 -0
- package/src/modules/billing/components/account/AccountView.tsx +101 -0
- package/src/modules/billing/components/account/accountRow.ts +70 -0
- package/src/modules/billing/components/customerNames.ts +62 -0
- package/src/modules/billing/data/entities.ts +656 -6
- package/src/modules/billing/data/entityIds.ts +9 -0
- package/src/modules/billing/data/validators.ts +388 -8
- package/src/modules/billing/events.ts +56 -3
- package/src/modules/billing/i18n/de.json +880 -0
- package/src/modules/billing/i18n/en.json +457 -21
- package/src/modules/billing/i18n/es.json +880 -0
- package/src/modules/billing/i18n/pl.json +458 -22
- package/src/modules/billing/index.ts +6 -2
- package/src/modules/billing/lib/accountFinancials.ts +84 -0
- package/src/modules/billing/lib/accountLock.ts +32 -0
- package/src/modules/billing/lib/accountStatus.ts +11 -9
- package/src/modules/billing/lib/accountSummary.ts +172 -0
- package/src/modules/billing/lib/allocation.ts +515 -0
- package/src/modules/billing/lib/allocationEvents.ts +26 -0
- package/src/modules/billing/lib/billPeriod.ts +19 -0
- package/src/modules/billing/lib/billRunEngine.ts +350 -150
- package/src/modules/billing/lib/billingSettings.ts +145 -0
- package/src/modules/billing/lib/commandRoute.ts +112 -0
- package/src/modules/billing/lib/customerLookup.ts +157 -0
- package/src/modules/billing/lib/dateOnly.ts +22 -0
- package/src/modules/billing/lib/documentParties.ts +23 -0
- package/src/modules/billing/lib/documentTypes.ts +37 -0
- package/src/modules/billing/lib/dueDate.ts +26 -0
- package/src/modules/billing/lib/iban.ts +36 -0
- package/src/modules/billing/lib/invoiceDelivery.ts +196 -0
- package/src/modules/billing/lib/invoiceLanguages.ts +57 -0
- package/src/modules/billing/lib/invoiceWriter.ts +162 -148
- package/src/modules/billing/lib/itemBillingState.ts +67 -0
- package/src/modules/billing/lib/itemPriceChange.ts +56 -0
- package/src/modules/billing/lib/itemSelector.ts +4 -1
- package/src/modules/billing/lib/lineAmounts.ts +61 -0
- package/src/modules/billing/lib/memoGuards.ts +41 -0
- package/src/modules/billing/lib/navigation.ts +64 -0
- package/src/modules/billing/lib/numbering.ts +174 -0
- package/src/modules/billing/lib/overdue.ts +60 -0
- package/src/modules/billing/lib/paymentCsv.ts +171 -0
- package/src/modules/billing/lib/paymentGateway.ts +52 -0
- package/src/modules/billing/lib/paymentImport.ts +228 -0
- package/src/modules/billing/lib/paymentMatcher.ts +88 -0
- package/src/modules/billing/lib/pdf/attachmentStore.ts +60 -0
- package/src/modules/billing/lib/pdf/fonts/NotoSans-Bold.ttf +0 -0
- package/src/modules/billing/lib/pdf/fonts/NotoSans-Regular.ttf +0 -0
- package/src/modules/billing/lib/pdf/fonts/OFL.txt +93 -0
- package/src/modules/billing/lib/pdf/fonts/embeddedFonts.ts +9 -0
- package/src/modules/billing/lib/pdf/invoiceModel.ts +329 -0
- package/src/modules/billing/lib/pdf/invoicePdf.ts +220 -0
- package/src/modules/billing/lib/pdf/invoiceTemplate.ts +338 -0
- package/src/modules/billing/lib/prepaidConfig.ts +27 -36
- package/src/modules/billing/lib/prepaidConsume.ts +29 -6
- package/src/modules/billing/lib/prepaidPeriodClose.ts +407 -239
- package/src/modules/billing/lib/prepaidTaxPoint.ts +76 -0
- package/src/modules/billing/lib/seeds/demo.ts +79 -6
- package/src/modules/billing/lib/seeds/paymentsSample.ts +136 -0
- package/src/modules/billing/lib/seeds/sellerSettings.ts +78 -0
- package/src/modules/billing/lib/servicePeriods.ts +226 -0
- package/src/modules/billing/lib/testRunCleanup.ts +42 -0
- package/src/modules/billing/lib/topupCapture.ts +3 -4
- package/src/modules/billing/lib/topupReceipt.ts +70 -103
- package/src/modules/billing/lib/usageRater.ts +1 -1
- package/src/modules/billing/lib/usageRunner.ts +17 -5
- package/src/modules/billing/lib/vat.ts +56 -0
- package/src/modules/billing/migrations/Migration20261114000000_billing_customer_kind.ts +25 -0
- package/src/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.ts +37 -0
- package/src/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.ts +132 -0
- package/src/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.ts +58 -0
- package/src/modules/billing/migrations/Migration20261117000000_billing_payments.ts +97 -0
- package/src/modules/billing/migrations/Migration20261118000000_billing_memos.ts +21 -0
- package/src/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.ts +39 -0
- package/src/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.ts +35 -0
- package/src/modules/billing/setup.ts +11 -237
- package/src/modules/billing/subscribers/credit-memo-auto-send.ts +15 -0
- package/src/modules/billing/subscribers/debit-memo-apply-credit.ts +15 -0
- package/src/modules/billing/subscribers/debit-memo-auto-send.ts +15 -0
- package/src/modules/billing/subscribers/invoice-apply-credit.ts +39 -0
- package/src/modules/billing/subscribers/invoice-auto-send.ts +46 -0
- package/src/modules/billing/subscribers/payment-captured-to-paid.ts +84 -62
- package/src/modules/billing/subscribers/prepaid-topup-captured.ts +4 -8
- package/dist/modules/billing/lib/invoiceStatus.js +0 -38
- package/dist/modules/billing/lib/invoiceStatus.js.map +0 -7
- package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +0 -97
- package/src/modules/billing/lib/invoiceStatus.ts +0 -61
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/lib/itemSelector.ts"],
|
|
4
|
-
"sourcesContent": ["import type { BillingItem } from '../data/entities'\nimport type { BillPeriod } from './billPeriod'\n\n/**\n * Decide whether a Billing Item contributes to a given bill period.\n *\n * Two anti-duplicate guards working together (spec \"Why two anti-duplicate\n * guards\"):\n *\n * 1. `billed_to_date` \u2014 per-item watermark. Set after a successful\n * real-mode run that emitted this item; prevents re-billing the\n * same item even if the period boundaries shift.\n * 2. Open-draft check \u2014 per-account, handled by the engine before it\n * even invokes this selector (see `billRunEngine`). Not this\n * module's concern.\n *\n * Per-type semantics from the spec:\n *\n * `one_time` \u2014 one shot per item, ever. Eligible only if\n * `billed_to_date IS NULL` AND the item's `[bill_start_date,\n * bill_end_date]` window overlaps the bill period.\n *\n * `recurring` \u2014 one line per full cycle. Eligible only when:\n * - `bill_start_date <= bill_period_start`\n * - AND (`bill_end_date IS NULL` OR `bill_end_date >= bill_period_end`)\n * - AND (`billed_to_date IS NULL` OR `billed_to_date < bill_period_end`)\n *\n * Mid-cycle starts / cancels are SKIPPED \u2014 the integrator is\n * responsible for posting a `one_time` proration line if the\n * partial period should be charged (spec \"Mid-cycle recurring\n * items\").\n *\n * `usage` \u2014 not handled here. Phase 3 owns usage rating end-to-end.\n *\n * All comparisons treat dates as midnight (consistent with `billPeriod`).\n */\n\nexport type SelectableItem = Pick<\n BillingItem,\n | 'id'\n | 'type'\n | 'billStartDate'\n | 'billEndDate'\n | 'billedToDate'\n | 'description'\n | 'rateJson'\n | 'isActive'\n | 'deletedAt'\n>\n\nexport type SelectionDecision =\n | { include: true }\n | { include: false; reason: string }\n\nfunction asTime(date: Date | null | undefined): number | null {\n if (!date) return null\n return new Date(date).getTime()\n}\n\nfunction isWithin(\n windowStart: Date,\n windowEnd: Date | null | undefined,\n point: Date,\n): boolean {\n const pointTime = asTime(point)!\n const startTime = asTime(windowStart)!\n if (pointTime < startTime) return false\n const endTime = asTime(windowEnd)\n if (endTime === null) return true\n return pointTime <= endTime\n}\n\nfunction overlapsPeriod(\n itemStart: Date,\n itemEnd: Date | null | undefined,\n period: BillPeriod,\n): boolean {\n const itemStartTime = asTime(itemStart)!\n const itemEndTime = asTime(itemEnd)\n const periodStartTime = asTime(period.periodStart)!\n const periodEndTime = asTime(period.periodEnd)!\n if (itemStartTime > periodEndTime) return false\n if (itemEndTime !== null && itemEndTime < periodStartTime) return false\n return true\n}\n\nexport function shouldIncludeItem(\n item: SelectableItem,\n period: BillPeriod,\n): SelectionDecision {\n if (!item.isActive) return { include: false, reason: 'item-inactive' }\n if (item.deletedAt) return { include: false, reason: 'item-soft-deleted' }\n\n switch (item.type) {\n case 'one_time': {\n if (item.billedToDate) {\n return { include: false, reason: 'one_time-already-billed' }\n }\n if (!overlapsPeriod(item.billStartDate, item.billEndDate, period)) {\n return { include: false, reason: 'one_time-window-outside-period' }\n }\n return { include: true }\n }\n case 'recurring': {\n const billedToTime = asTime(item.billedToDate)\n const periodEndTime = asTime(period.periodEnd)!\n if (billedToTime !== null && billedToTime >= periodEndTime) {\n return { include: false, reason: 'recurring-already-billed-through-period' }\n }\n const periodStartTime = asTime(period.periodStart)!\n const itemStartTime = asTime(item.billStartDate)!\n if (itemStartTime > periodStartTime) {\n return { include: false, reason: 'recurring-starts-mid-cycle' }\n }\n const itemEndTime = asTime(item.billEndDate)\n if (itemEndTime !== null && itemEndTime < periodEndTime) {\n return { include: false, reason: 'recurring-ends-mid-cycle' }\n }\n return { include: true }\n }\n case 'usage':\n // Phase 3 routes usage items through the dedicated usage rater.\n // The selector returns \"skip\" so the engine's recurring/one_time\n // path is unaffected.\n return { include: false, reason: 'usage-handled-by-rater' }\n default:\n return { include: false, reason: `unknown-type:${item.type}` }\n }\n}\n\n/**\n * Pure filter \u2014 given the items already loaded for an account, returns\n * those eligible for the bill period together with the decision reason\n * for diagnostics (the engine logs skipped items in the run outcome's\n * warnings on demand).\n */\nexport function selectItemsForPeriod<T extends SelectableItem>(\n items: readonly T[],\n period: BillPeriod,\n): { included: T[]; skipped: Array<{ item: T; reason: string }> } {\n const included: T[] = []\n const skipped: Array<{ item: T; reason: string }> = []\n for (const item of items) {\n const decision = shouldIncludeItem(item, period)\n if (decision.include) {\n included.push(item)\n } else {\n skipped.push({ item, reason: decision.reason })\n }\n }\n return { included, skipped }\n}\n\nexport { isWithin, overlapsPeriod }\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { BillingItem } from '../data/entities'\nimport type { BillPeriod } from './billPeriod'\n\n/**\n * Decide whether a Billing Item contributes to a given bill period.\n *\n * Two anti-duplicate guards working together (spec \"Why two anti-duplicate\n * guards\"):\n *\n * 1. `billed_to_date` \u2014 per-item watermark. Set after a successful\n * real-mode run that emitted this item; prevents re-billing the\n * same item even if the period boundaries shift.\n * 2. Open-draft check \u2014 per-account, handled by the engine before it\n * even invokes this selector (see `billRunEngine`). Not this\n * module's concern.\n *\n * Per-type semantics from the spec:\n *\n * `one_time` \u2014 one shot per item, ever. Eligible only if\n * `billed_to_date IS NULL` AND the item's `[bill_start_date,\n * bill_end_date]` window overlaps the bill period.\n *\n * `recurring` \u2014 used by PREPAID period close only (Spec A, A-D1). The\n * postpaid engine prices recurring items with `servicePeriods`\n * (in advance / in arrears, proration, credits). Here: one line per\n * full cycle. Eligible only when:\n * - `bill_start_date <= bill_period_start`\n * - AND (`bill_end_date IS NULL` OR `bill_end_date >= bill_period_end`)\n * - AND (`billed_to_date IS NULL` OR `billed_to_date < bill_period_end`)\n *\n * Mid-cycle starts / cancels are SKIPPED \u2014 the integrator is\n * responsible for posting a `one_time` proration line if the\n * partial period should be charged (spec \"Mid-cycle recurring\n * items\").\n *\n * `usage` \u2014 not handled here. Phase 3 owns usage rating end-to-end.\n *\n * All comparisons treat dates as midnight (consistent with `billPeriod`).\n */\n\nexport type SelectableItem = Pick<\n BillingItem,\n | 'id'\n | 'type'\n | 'billStartDate'\n | 'billEndDate'\n | 'billedToDate'\n | 'description'\n | 'rateJson'\n | 'isActive'\n | 'deletedAt'\n>\n\nexport type SelectionDecision =\n | { include: true }\n | { include: false; reason: string }\n\nfunction asTime(date: Date | null | undefined): number | null {\n if (!date) return null\n return new Date(date).getTime()\n}\n\nfunction isWithin(\n windowStart: Date,\n windowEnd: Date | null | undefined,\n point: Date,\n): boolean {\n const pointTime = asTime(point)!\n const startTime = asTime(windowStart)!\n if (pointTime < startTime) return false\n const endTime = asTime(windowEnd)\n if (endTime === null) return true\n return pointTime <= endTime\n}\n\nfunction overlapsPeriod(\n itemStart: Date,\n itemEnd: Date | null | undefined,\n period: BillPeriod,\n): boolean {\n const itemStartTime = asTime(itemStart)!\n const itemEndTime = asTime(itemEnd)\n const periodStartTime = asTime(period.periodStart)!\n const periodEndTime = asTime(period.periodEnd)!\n if (itemStartTime > periodEndTime) return false\n if (itemEndTime !== null && itemEndTime < periodStartTime) return false\n return true\n}\n\nexport function shouldIncludeItem(\n item: SelectableItem,\n period: BillPeriod,\n): SelectionDecision {\n if (!item.isActive) return { include: false, reason: 'item-inactive' }\n if (item.deletedAt) return { include: false, reason: 'item-soft-deleted' }\n\n switch (item.type) {\n case 'one_time': {\n if (item.billedToDate) {\n return { include: false, reason: 'one_time-already-billed' }\n }\n if (!overlapsPeriod(item.billStartDate, item.billEndDate, period)) {\n return { include: false, reason: 'one_time-window-outside-period' }\n }\n return { include: true }\n }\n case 'recurring': {\n const billedToTime = asTime(item.billedToDate)\n const periodEndTime = asTime(period.periodEnd)!\n if (billedToTime !== null && billedToTime >= periodEndTime) {\n return { include: false, reason: 'recurring-already-billed-through-period' }\n }\n const periodStartTime = asTime(period.periodStart)!\n const itemStartTime = asTime(item.billStartDate)!\n if (itemStartTime > periodStartTime) {\n return { include: false, reason: 'recurring-starts-mid-cycle' }\n }\n const itemEndTime = asTime(item.billEndDate)\n if (itemEndTime !== null && itemEndTime < periodEndTime) {\n return { include: false, reason: 'recurring-ends-mid-cycle' }\n }\n return { include: true }\n }\n case 'usage':\n // Phase 3 routes usage items through the dedicated usage rater.\n // The selector returns \"skip\" so the engine's recurring/one_time\n // path is unaffected.\n return { include: false, reason: 'usage-handled-by-rater' }\n default:\n return { include: false, reason: `unknown-type:${item.type}` }\n }\n}\n\n/**\n * Pure filter \u2014 given the items already loaded for an account, returns\n * those eligible for the bill period together with the decision reason\n * for diagnostics (the engine logs skipped items in the run outcome's\n * warnings on demand).\n */\nexport function selectItemsForPeriod<T extends SelectableItem>(\n items: readonly T[],\n period: BillPeriod,\n): { included: T[]; skipped: Array<{ item: T; reason: string }> } {\n const included: T[] = []\n const skipped: Array<{ item: T; reason: string }> = []\n for (const item of items) {\n const decision = shouldIncludeItem(item, period)\n if (decision.include) {\n included.push(item)\n } else {\n skipped.push({ item, reason: decision.reason })\n }\n }\n return { included, skipped }\n}\n\nexport { isWithin, overlapsPeriod }\n"],
|
|
5
|
+
"mappings": "AAyDA,SAAS,OAAO,MAA8C;AAC5D,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,IAAI,KAAK,IAAI,EAAE,QAAQ;AAChC;AAEA,SAAS,SACP,aACA,WACA,OACS;AACT,QAAM,YAAY,OAAO,KAAK;AAC9B,QAAM,YAAY,OAAO,WAAW;AACpC,MAAI,YAAY,UAAW,QAAO;AAClC,QAAM,UAAU,OAAO,SAAS;AAChC,MAAI,YAAY,KAAM,QAAO;AAC7B,SAAO,aAAa;AACtB;AAEA,SAAS,eACP,WACA,SACA,QACS;AACT,QAAM,gBAAgB,OAAO,SAAS;AACtC,QAAM,cAAc,OAAO,OAAO;AAClC,QAAM,kBAAkB,OAAO,OAAO,WAAW;AACjD,QAAM,gBAAgB,OAAO,OAAO,SAAS;AAC7C,MAAI,gBAAgB,cAAe,QAAO;AAC1C,MAAI,gBAAgB,QAAQ,cAAc,gBAAiB,QAAO;AAClE,SAAO;AACT;AAEO,SAAS,kBACd,MACA,QACmB;AACnB,MAAI,CAAC,KAAK,SAAU,QAAO,EAAE,SAAS,OAAO,QAAQ,gBAAgB;AACrE,MAAI,KAAK,UAAW,QAAO,EAAE,SAAS,OAAO,QAAQ,oBAAoB;AAEzE,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK,YAAY;AACf,UAAI,KAAK,cAAc;AACrB,eAAO,EAAE,SAAS,OAAO,QAAQ,0BAA0B;AAAA,MAC7D;AACA,UAAI,CAAC,eAAe,KAAK,eAAe,KAAK,aAAa,MAAM,GAAG;AACjE,eAAO,EAAE,SAAS,OAAO,QAAQ,iCAAiC;AAAA,MACpE;AACA,aAAO,EAAE,SAAS,KAAK;AAAA,IACzB;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,eAAe,OAAO,KAAK,YAAY;AAC7C,YAAM,gBAAgB,OAAO,OAAO,SAAS;AAC7C,UAAI,iBAAiB,QAAQ,gBAAgB,eAAe;AAC1D,eAAO,EAAE,SAAS,OAAO,QAAQ,0CAA0C;AAAA,MAC7E;AACA,YAAM,kBAAkB,OAAO,OAAO,WAAW;AACjD,YAAM,gBAAgB,OAAO,KAAK,aAAa;AAC/C,UAAI,gBAAgB,iBAAiB;AACnC,eAAO,EAAE,SAAS,OAAO,QAAQ,6BAA6B;AAAA,MAChE;AACA,YAAM,cAAc,OAAO,KAAK,WAAW;AAC3C,UAAI,gBAAgB,QAAQ,cAAc,eAAe;AACvD,eAAO,EAAE,SAAS,OAAO,QAAQ,2BAA2B;AAAA,MAC9D;AACA,aAAO,EAAE,SAAS,KAAK;AAAA,IACzB;AAAA,IACA,KAAK;AAIH,aAAO,EAAE,SAAS,OAAO,QAAQ,yBAAyB;AAAA,IAC5D;AACE,aAAO,EAAE,SAAS,OAAO,QAAQ,gBAAgB,KAAK,IAAI,GAAG;AAAA,EACjE;AACF;AAQO,SAAS,qBACd,OACA,QACgE;AAChE,QAAM,WAAgB,CAAC;AACvB,QAAM,UAA8C,CAAC;AACrD,aAAW,QAAQ,OAAO;AACxB,UAAM,WAAW,kBAAkB,MAAM,MAAM;AAC/C,QAAI,SAAS,SAAS;AACpB,eAAS,KAAK,IAAI;AAAA,IACpB,OAAO;AACL,cAAQ,KAAK,EAAE,MAAM,QAAQ,SAAS,OAAO,CAAC;AAAA,IAChD;AAAA,EACF;AACA,SAAO,EAAE,UAAU,QAAQ;AAC7B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { lineAmountsFromGross, lineAmountsFromNet } from "./vat.js";
|
|
2
|
+
function toNumber(value) {
|
|
3
|
+
const n = typeof value === "number" ? value : Number.parseFloat(value);
|
|
4
|
+
return Number.isFinite(n) ? n : 0;
|
|
5
|
+
}
|
|
6
|
+
function lineVat(input) {
|
|
7
|
+
const amount = toNumber(input.amount);
|
|
8
|
+
const rate = toNumber(input.taxRate);
|
|
9
|
+
return input.priceBasis === "gross" ? lineAmountsFromGross(amount, rate) : lineAmountsFromNet(amount, rate);
|
|
10
|
+
}
|
|
11
|
+
function priceBasisOf(value) {
|
|
12
|
+
return value === "gross" ? "gross" : "net";
|
|
13
|
+
}
|
|
14
|
+
function unitPriceNetOf(unitPrice, priceBasis, taxRate) {
|
|
15
|
+
const unit = toNumber(unitPrice);
|
|
16
|
+
if (priceBasis !== "gross") return unit.toFixed(4);
|
|
17
|
+
const rate = toNumber(taxRate);
|
|
18
|
+
const net = rate > 0 ? unit / (1 + rate / 100) : unit;
|
|
19
|
+
const sign = net < 0 ? -1 : 1;
|
|
20
|
+
return (sign * Math.round((Math.abs(net) + Number.EPSILON) * 1e4) / 1e4).toFixed(4);
|
|
21
|
+
}
|
|
22
|
+
function counterpartAmount(amount, priceBasis, taxRate) {
|
|
23
|
+
const vat = lineVat({ amount, priceBasis, taxRate });
|
|
24
|
+
return priceBasis === "gross" ? vat.net : vat.gross;
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
counterpartAmount,
|
|
28
|
+
lineVat,
|
|
29
|
+
priceBasisOf,
|
|
30
|
+
unitPriceNetOf
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=lineAmounts.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/lineAmounts.ts"],
|
|
4
|
+
"sourcesContent": ["import { lineAmountsFromGross, lineAmountsFromNet, type VatAmounts } from './vat'\n\n/**\n * Basis-aware line VAT \u2014 the single entry point for every billing line\n * (Spec F, P-D3): invoice drafts, dry / test previews and prepaid debits.\n *\n * An item price is stored as typed, on its `price_basis`:\n * - `net`: tax = round2(net \u00D7 rate), gross = net + tax\n * - `gross`: net = round2(gross / (1 + rate)), tax = gross \u2212 net\n * so a gross the operator typed is exactly the gross on the document.\n * Rounding happens once per line, on the extended amount\n * (unit \u00D7 quantity \u00D7 proration), never per unit.\n */\n\nexport type PriceBasis = 'net' | 'gross'\n\nexport type LineVatInput = {\n /** Extended line amount on `priceBasis` (unit \u00D7 quantity \u00D7 proration). */\n amount: number | string\n priceBasis: PriceBasis\n /** VAT rate in percent. */\n taxRate: number | string\n}\n\nfunction toNumber(value: number | string): number {\n const n = typeof value === 'number' ? value : Number.parseFloat(value)\n return Number.isFinite(n) ? n : 0\n}\n\nexport function lineVat(input: LineVatInput): VatAmounts {\n const amount = toNumber(input.amount)\n const rate = toNumber(input.taxRate)\n return input.priceBasis === 'gross' ? lineAmountsFromGross(amount, rate) : lineAmountsFromNet(amount, rate)\n}\n\n/** Resolve a stored basis value (anything but `gross` reads as `net`). */\nexport function priceBasisOf(value: unknown): PriceBasis {\n return value === 'gross' ? 'gross' : 'net'\n}\n\n/**\n * Net unit price for `billing_invoice_lines.unit_price_net` (display only,\n * 4 dp). Net basis keeps the typed unit price; gross basis derives it.\n */\nexport function unitPriceNetOf(unitPrice: number | string, priceBasis: PriceBasis, taxRate: number | string): string {\n const unit = toNumber(unitPrice)\n if (priceBasis !== 'gross') return unit.toFixed(4)\n const rate = toNumber(taxRate)\n const net = rate > 0 ? unit / (1 + rate / 100) : unit\n const sign = net < 0 ? -1 : 1\n return ((sign * Math.round((Math.abs(net) + Number.EPSILON) * 10000)) / 10000).toFixed(4)\n}\n\n/**\n * The counterpart of a typed unit price, for the ItemForm preview: the gross\n * of a net price or the net of a gross price (2 dp, same rule as the lines).\n */\nexport function counterpartAmount(amount: number | string, priceBasis: PriceBasis, taxRate: number | string): number {\n const vat = lineVat({ amount, priceBasis, taxRate })\n return priceBasis === 'gross' ? vat.net : vat.gross\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,sBAAsB,0BAA2C;AAwB1E,SAAS,SAAS,OAAgC;AAChD,QAAM,IAAI,OAAO,UAAU,WAAW,QAAQ,OAAO,WAAW,KAAK;AACrE,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAEO,SAAS,QAAQ,OAAiC;AACvD,QAAM,SAAS,SAAS,MAAM,MAAM;AACpC,QAAM,OAAO,SAAS,MAAM,OAAO;AACnC,SAAO,MAAM,eAAe,UAAU,qBAAqB,QAAQ,IAAI,IAAI,mBAAmB,QAAQ,IAAI;AAC5G;AAGO,SAAS,aAAa,OAA4B;AACvD,SAAO,UAAU,UAAU,UAAU;AACvC;AAMO,SAAS,eAAe,WAA4B,YAAwB,SAAkC;AACnH,QAAM,OAAO,SAAS,SAAS;AAC/B,MAAI,eAAe,QAAS,QAAO,KAAK,QAAQ,CAAC;AACjD,QAAM,OAAO,SAAS,OAAO;AAC7B,QAAM,MAAM,OAAO,IAAI,QAAQ,IAAI,OAAO,OAAO;AACjD,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAS,OAAO,KAAK,OAAO,KAAK,IAAI,GAAG,IAAI,OAAO,WAAW,GAAK,IAAK,KAAO,QAAQ,CAAC;AAC1F;AAMO,SAAS,kBAAkB,QAAyB,YAAwB,SAAkC;AACnH,QAAM,MAAM,QAAQ,EAAE,QAAQ,YAAY,QAAQ,CAAC;AACnD,SAAO,eAAe,UAAU,IAAI,MAAM,IAAI;AAChD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { toCents } from "./allocation.js";
|
|
2
|
+
function creditHeadroomCents(originalGrossCents, totals) {
|
|
3
|
+
return originalGrossCents + totals.debitCents - totals.creditCents;
|
|
4
|
+
}
|
|
5
|
+
function creditFits(originalGrossCents, totals, newCreditCents) {
|
|
6
|
+
return newCreditCents <= creditHeadroomCents(originalGrossCents, totals);
|
|
7
|
+
}
|
|
8
|
+
async function loadCorrectionTotals(em, scope, originalId, opts = {}) {
|
|
9
|
+
const rows = await em.execute(
|
|
10
|
+
`SELECT document_type, COALESCE(SUM(total_gross), 0) AS gross
|
|
11
|
+
FROM billing_invoices
|
|
12
|
+
WHERE corrects_invoice_id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL
|
|
13
|
+
AND status <> 'draft' AND test_run = false AND id <> ?
|
|
14
|
+
GROUP BY document_type`,
|
|
15
|
+
[originalId, scope.tenantId, scope.organizationId, opts.excludeId ?? "00000000-0000-0000-0000-000000000000"]
|
|
16
|
+
);
|
|
17
|
+
const of = (type) => toCents(rows?.find((r) => r.document_type === type)?.gross ?? "0");
|
|
18
|
+
return { creditCents: of("credit_memo"), debitCents: of("debit_memo") };
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
creditFits,
|
|
22
|
+
creditHeadroomCents,
|
|
23
|
+
loadCorrectionTotals
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=memoGuards.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/memoGuards.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { toCents } from './allocation'\n\n/**\n * Credit memo guard (Spec D, P-D4). Pure core + one aggregate query.\n *\n * \u03A3 gross of posted credit memos on the original (incl. the new one)\n * \u2264 original gross + \u03A3 gross of posted debit memos on it\n *\n * Evaluated inside the per-account lock at post (authoritative) and,\n * without the lock, when a draft memo is created (early feedback).\n */\n\nexport type CorrectionTotals = { creditCents: number; debitCents: number }\n\nexport function creditHeadroomCents(originalGrossCents: number, totals: CorrectionTotals): number {\n return originalGrossCents + totals.debitCents - totals.creditCents\n}\n\nexport function creditFits(originalGrossCents: number, totals: CorrectionTotals, newCreditCents: number): boolean {\n return newCreditCents <= creditHeadroomCents(originalGrossCents, totals)\n}\n\n/** \u03A3 gross of POSTED (non-draft) memos correcting an invoice, per type. */\nexport async function loadCorrectionTotals(\n em: EntityManager,\n scope: { tenantId: string; organizationId: string },\n originalId: string,\n opts: { excludeId?: string } = {},\n): Promise<CorrectionTotals> {\n const rows = (await em.execute(\n `SELECT document_type, COALESCE(SUM(total_gross), 0) AS gross\n FROM billing_invoices\n WHERE corrects_invoice_id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL\n AND status <> 'draft' AND test_run = false AND id <> ?\n GROUP BY document_type`,\n [originalId, scope.tenantId, scope.organizationId, opts.excludeId ?? '00000000-0000-0000-0000-000000000000'],\n )) as unknown as Array<{ document_type: string; gross: string }>\n const of = (type: string) => toCents(rows?.find((r) => r.document_type === type)?.gross ?? '0')\n return { creditCents: of('credit_memo'), debitCents: of('debit_memo') }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,eAAe;AAcjB,SAAS,oBAAoB,oBAA4B,QAAkC;AAChG,SAAO,qBAAqB,OAAO,aAAa,OAAO;AACzD;AAEO,SAAS,WAAW,oBAA4B,QAA0B,gBAAiC;AAChH,SAAO,kBAAkB,oBAAoB,oBAAoB,MAAM;AACzE;AAGA,eAAsB,qBACpB,IACA,OACA,YACA,OAA+B,CAAC,GACL;AAC3B,QAAM,OAAQ,MAAM,GAAG;AAAA,IACrB;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,CAAC,YAAY,MAAM,UAAU,MAAM,gBAAgB,KAAK,aAAa,sCAAsC;AAAA,EAC7G;AACA,QAAM,KAAK,CAAC,SAAiB,QAAQ,MAAM,KAAK,CAAC,MAAM,EAAE,kBAAkB,IAAI,GAAG,SAAS,GAAG;AAC9F,SAAO,EAAE,aAAa,GAAG,aAAa,GAAG,YAAY,GAAG,YAAY,EAAE;AACxE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const ITEMS_LIST_HREF = "/backend/billing/items";
|
|
2
|
+
const FROM_ITEMS_LIST = "items";
|
|
3
|
+
const ACCOUNT_TABS = ["overview", "items", "usage", "invoices", "payments", "balance", "statements", "runs"];
|
|
4
|
+
function isAccountTab(value) {
|
|
5
|
+
return typeof value === "string" && ACCOUNT_TABS.includes(value);
|
|
6
|
+
}
|
|
7
|
+
function accountHref(billAccountId, tab) {
|
|
8
|
+
const base = `/backend/billing/accounts/${encodeURIComponent(billAccountId)}`;
|
|
9
|
+
return tab && tab !== "overview" ? `${base}?tab=${tab}` : base;
|
|
10
|
+
}
|
|
11
|
+
function resolveItemReturnTarget(params) {
|
|
12
|
+
if (params.from === FROM_ITEMS_LIST) return ITEMS_LIST_HREF;
|
|
13
|
+
const accountId = (params.billAccountId ?? "").trim();
|
|
14
|
+
return accountId ? accountHref(accountId, "items") : ITEMS_LIST_HREF;
|
|
15
|
+
}
|
|
16
|
+
function itemDetailHref(itemId, opts = {}) {
|
|
17
|
+
const base = `${ITEMS_LIST_HREF}/${encodeURIComponent(itemId)}`;
|
|
18
|
+
return opts.fromItemsList ? `${base}?from=${FROM_ITEMS_LIST}` : base;
|
|
19
|
+
}
|
|
20
|
+
const INVOICES_LIST_HREF = "/backend/billing/invoices";
|
|
21
|
+
const FROM_ACCOUNT = "account";
|
|
22
|
+
function invoiceDetailHref(invoiceId, opts = {}) {
|
|
23
|
+
const base = `${INVOICES_LIST_HREF}/${encodeURIComponent(invoiceId)}`;
|
|
24
|
+
const accountId = (opts.fromAccountId ?? "").trim();
|
|
25
|
+
return accountId ? `${base}?from=${FROM_ACCOUNT}&accountId=${encodeURIComponent(accountId)}` : base;
|
|
26
|
+
}
|
|
27
|
+
function resolveInvoiceReturnTarget(params) {
|
|
28
|
+
const accountId = (params.accountId ?? "").trim();
|
|
29
|
+
return params.from === FROM_ACCOUNT && accountId ? accountHref(accountId, "invoices") : INVOICES_LIST_HREF;
|
|
30
|
+
}
|
|
31
|
+
function invoicePdfHref(invoiceId) {
|
|
32
|
+
return `/api/billing/invoices/${encodeURIComponent(invoiceId)}/pdf`;
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
ACCOUNT_TABS,
|
|
36
|
+
FROM_ACCOUNT,
|
|
37
|
+
FROM_ITEMS_LIST,
|
|
38
|
+
INVOICES_LIST_HREF,
|
|
39
|
+
ITEMS_LIST_HREF,
|
|
40
|
+
accountHref,
|
|
41
|
+
invoiceDetailHref,
|
|
42
|
+
invoicePdfHref,
|
|
43
|
+
isAccountTab,
|
|
44
|
+
itemDetailHref,
|
|
45
|
+
resolveInvoiceReturnTarget,
|
|
46
|
+
resolveItemReturnTarget
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=navigation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/navigation.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Where an item page returns to (Spec E, D23).\n *\n * Billing items always belong to an account, so the account is the natural\n * parent: back / after-save / after-delete land there. The only exception is\n * an item opened from the global Items list (`?from=items`), which returns\n * to that list so the operator keeps their place.\n */\n\nexport const ITEMS_LIST_HREF = '/backend/billing/items'\n\n/** Query value the global Items list appends when opening an item. */\nexport const FROM_ITEMS_LIST = 'items'\n\n/** Tabs of the Billing Account workspace (Spec E / Spec F, F8), deep-linkable via `?tab=`. */\nexport const ACCOUNT_TABS = ['overview', 'items', 'usage', 'invoices', 'payments', 'balance', 'statements', 'runs'] as const\nexport type AccountTab = (typeof ACCOUNT_TABS)[number]\n\nexport function isAccountTab(value: unknown): value is AccountTab {\n return typeof value === 'string' && (ACCOUNT_TABS as readonly string[]).includes(value)\n}\n\n/** The account workspace, optionally on one tab (`overview` is the default and is left out). */\nexport function accountHref(billAccountId: string, tab?: AccountTab): string {\n const base = `/backend/billing/accounts/${encodeURIComponent(billAccountId)}`\n return tab && tab !== 'overview' ? `${base}?tab=${tab}` : base\n}\n\nexport function resolveItemReturnTarget(params: {\n from?: string | null\n billAccountId?: string | null\n}): string {\n if (params.from === FROM_ITEMS_LIST) return ITEMS_LIST_HREF\n const accountId = (params.billAccountId ?? '').trim()\n return accountId ? accountHref(accountId, 'items') : ITEMS_LIST_HREF\n}\n\nexport function itemDetailHref(itemId: string, opts: { fromItemsList?: boolean } = {}): string {\n const base = `${ITEMS_LIST_HREF}/${encodeURIComponent(itemId)}`\n return opts.fromItemsList ? `${base}?from=${FROM_ITEMS_LIST}` : base\n}\n\n/**\n * Where an invoice detail page returns to (Spec B): the account it was\n * opened from (`?from=account`), otherwise the global Invoices list.\n */\nexport const INVOICES_LIST_HREF = '/backend/billing/invoices'\nexport const FROM_ACCOUNT = 'account'\n\nexport function invoiceDetailHref(invoiceId: string, opts: { fromAccountId?: string | null } = {}): string {\n const base = `${INVOICES_LIST_HREF}/${encodeURIComponent(invoiceId)}`\n const accountId = (opts.fromAccountId ?? '').trim()\n return accountId ? `${base}?from=${FROM_ACCOUNT}&accountId=${encodeURIComponent(accountId)}` : base\n}\n\nexport function resolveInvoiceReturnTarget(params: { from?: string | null; accountId?: string | null }): string {\n const accountId = (params.accountId ?? '').trim()\n return params.from === FROM_ACCOUNT && accountId ? accountHref(accountId, 'invoices') : INVOICES_LIST_HREF\n}\n\n/** PDF of a billing document \u2014 frozen when posted, a watermarked preview otherwise. */\nexport function invoicePdfHref(invoiceId: string): string {\n return `/api/billing/invoices/${encodeURIComponent(invoiceId)}/pdf`\n}\n"],
|
|
5
|
+
"mappings": "AASO,MAAM,kBAAkB;AAGxB,MAAM,kBAAkB;AAGxB,MAAM,eAAe,CAAC,YAAY,SAAS,SAAS,YAAY,YAAY,WAAW,cAAc,MAAM;AAG3G,SAAS,aAAa,OAAqC;AAChE,SAAO,OAAO,UAAU,YAAa,aAAmC,SAAS,KAAK;AACxF;AAGO,SAAS,YAAY,eAAuB,KAA0B;AAC3E,QAAM,OAAO,6BAA6B,mBAAmB,aAAa,CAAC;AAC3E,SAAO,OAAO,QAAQ,aAAa,GAAG,IAAI,QAAQ,GAAG,KAAK;AAC5D;AAEO,SAAS,wBAAwB,QAG7B;AACT,MAAI,OAAO,SAAS,gBAAiB,QAAO;AAC5C,QAAM,aAAa,OAAO,iBAAiB,IAAI,KAAK;AACpD,SAAO,YAAY,YAAY,WAAW,OAAO,IAAI;AACvD;AAEO,SAAS,eAAe,QAAgB,OAAoC,CAAC,GAAW;AAC7F,QAAM,OAAO,GAAG,eAAe,IAAI,mBAAmB,MAAM,CAAC;AAC7D,SAAO,KAAK,gBAAgB,GAAG,IAAI,SAAS,eAAe,KAAK;AAClE;AAMO,MAAM,qBAAqB;AAC3B,MAAM,eAAe;AAErB,SAAS,kBAAkB,WAAmB,OAA0C,CAAC,GAAW;AACzG,QAAM,OAAO,GAAG,kBAAkB,IAAI,mBAAmB,SAAS,CAAC;AACnE,QAAM,aAAa,KAAK,iBAAiB,IAAI,KAAK;AAClD,SAAO,YAAY,GAAG,IAAI,SAAS,YAAY,cAAc,mBAAmB,SAAS,CAAC,KAAK;AACjG;AAEO,SAAS,2BAA2B,QAAqE;AAC9G,QAAM,aAAa,OAAO,aAAa,IAAI,KAAK;AAChD,SAAO,OAAO,SAAS,gBAAgB,YAAY,YAAY,WAAW,UAAU,IAAI;AAC1F;AAGO,SAAS,eAAe,WAA2B;AACxD,SAAO,yBAAyB,mBAAmB,SAAS,CAAC;AAC/D;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const DOCUMENT_SERIES = ["invoice", "credit_memo", "receipt"];
|
|
2
|
+
const DEFAULT_NUMBER_MASKS = {
|
|
3
|
+
invoice: "INV/{YYYY}/{MM}/{SEQUENCE_NUM_MONTH:4}",
|
|
4
|
+
credit_memo: "CN/{YYYY}/{MM}/{SEQUENCE_NUM_MONTH:4}",
|
|
5
|
+
receipt: "RC/{YYYY}/{MM}/{SEQUENCE_NUM_MONTH:4}"
|
|
6
|
+
};
|
|
7
|
+
const MAX_MASK_LENGTH = 64;
|
|
8
|
+
const TOKEN_RE = /\{([A-Z_]+)(?::(\d{1,2}))?\}/g;
|
|
9
|
+
const DATE_TOKENS = /* @__PURE__ */ new Set(["YYYY", "YY", "MM", "DD"]);
|
|
10
|
+
const SEQUENCE_TOKENS = {
|
|
11
|
+
SEQUENCE_NUM: "none",
|
|
12
|
+
SEQUENCE_NUM_YEAR: "year",
|
|
13
|
+
SEQUENCE_NUM_MONTH: "month"
|
|
14
|
+
};
|
|
15
|
+
function tokensOf(mask) {
|
|
16
|
+
return Array.from(mask.matchAll(TOKEN_RE), (m) => ({ name: m[1], pad: m[2] ? Number(m[2]) : 0 }));
|
|
17
|
+
}
|
|
18
|
+
function validateMask(mask) {
|
|
19
|
+
const trimmed = (mask ?? "").trim();
|
|
20
|
+
if (!trimmed) return ["mask_empty"];
|
|
21
|
+
const errors = [];
|
|
22
|
+
if (trimmed.length > MAX_MASK_LENGTH) errors.push("mask_too_long");
|
|
23
|
+
const tokens = tokensOf(trimmed);
|
|
24
|
+
const stripped = trimmed.replace(TOKEN_RE, "");
|
|
25
|
+
if (/[{}]/.test(stripped) || tokens.some((t) => !DATE_TOKENS.has(t.name) && !(t.name in SEQUENCE_TOKENS))) {
|
|
26
|
+
errors.push("mask_unknown_token");
|
|
27
|
+
}
|
|
28
|
+
const sequences = tokens.filter((t) => t.name in SEQUENCE_TOKENS);
|
|
29
|
+
if (sequences.length === 0) errors.push("mask_missing_sequence");
|
|
30
|
+
if (sequences.length > 1) errors.push("mask_multiple_sequences");
|
|
31
|
+
const hasYear = tokens.some((t) => t.name === "YYYY" || t.name === "YY");
|
|
32
|
+
const hasMonth = tokens.some((t) => t.name === "MM");
|
|
33
|
+
const scope = sequences[0] ? SEQUENCE_TOKENS[sequences[0].name] : "none";
|
|
34
|
+
if (scope === "year" && !hasYear) errors.push("mask_year_scope_needs_year");
|
|
35
|
+
if (scope === "month" && (!hasYear || !hasMonth)) errors.push("mask_month_scope_needs_year_and_month");
|
|
36
|
+
return errors;
|
|
37
|
+
}
|
|
38
|
+
function parseMask(mask) {
|
|
39
|
+
const tokens = tokensOf(mask);
|
|
40
|
+
const sequence = tokens.find((t) => t.name in SEQUENCE_TOKENS);
|
|
41
|
+
return {
|
|
42
|
+
scope: sequence ? SEQUENCE_TOKENS[sequence.name] : "none",
|
|
43
|
+
pad: sequence?.pad ?? 0,
|
|
44
|
+
hasYear: tokens.some((t) => t.name === "YYYY" || t.name === "YY"),
|
|
45
|
+
hasMonth: tokens.some((t) => t.name === "MM")
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function datePart(date, part) {
|
|
49
|
+
const d = new Date(date);
|
|
50
|
+
switch (part) {
|
|
51
|
+
case "YYYY":
|
|
52
|
+
return String(d.getUTCFullYear());
|
|
53
|
+
case "YY":
|
|
54
|
+
return String(d.getUTCFullYear()).slice(-2);
|
|
55
|
+
case "MM":
|
|
56
|
+
return String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
57
|
+
case "DD":
|
|
58
|
+
return String(d.getUTCDate()).padStart(2, "0");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function sequenceScopeKey(mask, issueDate) {
|
|
62
|
+
const { scope } = parseMask(mask);
|
|
63
|
+
if (scope === "year") return datePart(issueDate, "YYYY");
|
|
64
|
+
if (scope === "month") return `${datePart(issueDate, "YYYY")}-${datePart(issueDate, "MM")}`;
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
function renderNumber(mask, params) {
|
|
68
|
+
return mask.replace(TOKEN_RE, (_match, name, pad) => {
|
|
69
|
+
if (name in SEQUENCE_TOKENS) return String(params.seq).padStart(pad ? Number(pad) : 0, "0");
|
|
70
|
+
if (DATE_TOKENS.has(name)) return datePart(params.issueDate, name);
|
|
71
|
+
return _match;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function maskFor(masks, series) {
|
|
75
|
+
const candidate = masks?.[series];
|
|
76
|
+
return candidate && validateMask(candidate).length === 0 ? candidate : DEFAULT_NUMBER_MASKS[series];
|
|
77
|
+
}
|
|
78
|
+
function randomHex(length) {
|
|
79
|
+
let out = "";
|
|
80
|
+
while (out.length < length) out += Math.floor(Math.random() * 4294967296).toString(16).padStart(8, "0");
|
|
81
|
+
return out.slice(0, length);
|
|
82
|
+
}
|
|
83
|
+
function draftNumber() {
|
|
84
|
+
return `DRAFT-${randomHex(8).toUpperCase()}`;
|
|
85
|
+
}
|
|
86
|
+
function testNumber() {
|
|
87
|
+
return `TEST-${Date.now().toString(16)}-${randomHex(8)}`;
|
|
88
|
+
}
|
|
89
|
+
async function claimDocumentNumber(em, scope, series, mask, issueDate) {
|
|
90
|
+
const scopeKey = sequenceScopeKey(mask, issueDate);
|
|
91
|
+
const now = /* @__PURE__ */ new Date();
|
|
92
|
+
const rows = await em.execute(
|
|
93
|
+
`INSERT INTO billing_document_sequences
|
|
94
|
+
(tenant_id, organization_id, series, scope_key, next_value, created_at, updated_at)
|
|
95
|
+
VALUES (?, ?, ?, ?, 2, ?, ?)
|
|
96
|
+
ON CONFLICT (tenant_id, organization_id, series, scope_key)
|
|
97
|
+
DO UPDATE SET next_value = billing_document_sequences.next_value + 1, updated_at = EXCLUDED.updated_at
|
|
98
|
+
RETURNING next_value - 1 AS seq`,
|
|
99
|
+
[scope.tenantId, scope.organizationId, series, scopeKey, now, now]
|
|
100
|
+
);
|
|
101
|
+
const seq = Number(rows?.[0]?.seq);
|
|
102
|
+
if (!Number.isFinite(seq) || seq < 1) {
|
|
103
|
+
throw new Error(`Failed to claim a ${series} number`);
|
|
104
|
+
}
|
|
105
|
+
return { number: renderNumber(mask, { seq, issueDate }), seq, scopeKey };
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
DEFAULT_NUMBER_MASKS,
|
|
109
|
+
DOCUMENT_SERIES,
|
|
110
|
+
MAX_MASK_LENGTH,
|
|
111
|
+
claimDocumentNumber,
|
|
112
|
+
draftNumber,
|
|
113
|
+
maskFor,
|
|
114
|
+
parseMask,
|
|
115
|
+
renderNumber,
|
|
116
|
+
sequenceScopeKey,
|
|
117
|
+
testNumber,
|
|
118
|
+
validateMask
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=numbering.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/numbering.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\n\n/**\n * Document numbering (Spec 0, decision E-D6 / P-D3).\n *\n * Each series has a mask, e.g. `INV/{YYYY}/{MM}/{SEQUENCE_NUM_MONTH:4}`:\n * {YYYY} {YY} {MM} {DD} \u2014 parts of the issue date\n * {SEQUENCE_NUM} \u2014 counter never resets\n * {SEQUENCE_NUM_YEAR} \u2014 counter resets every year\n * {SEQUENCE_NUM_MONTH} \u2014 counter resets every month\n * \u2026:N \u2014 optional zero-pad to N digits\n *\n * The counter row is `(tenant, org, series, scope_key)` where scope_key is\n * '' / 'YYYY' / 'YYYY-MM'. A number is claimed only at post, by one upsert\n * inside the caller's transaction \u2014 a rollback releases it (gap-free) and\n * concurrent posts serialize on the row.\n */\n\nexport const DOCUMENT_SERIES = ['invoice', 'credit_memo', 'receipt'] as const\nexport type DocumentSeries = (typeof DOCUMENT_SERIES)[number]\n\nexport const DEFAULT_NUMBER_MASKS: Record<DocumentSeries, string> = {\n invoice: 'INV/{YYYY}/{MM}/{SEQUENCE_NUM_MONTH:4}',\n credit_memo: 'CN/{YYYY}/{MM}/{SEQUENCE_NUM_MONTH:4}',\n receipt: 'RC/{YYYY}/{MM}/{SEQUENCE_NUM_MONTH:4}',\n}\n\nexport type SequenceScope = 'none' | 'year' | 'month'\n\nexport type ParsedMask = {\n scope: SequenceScope\n pad: number\n hasYear: boolean\n hasMonth: boolean\n}\n\nexport type MaskError =\n | 'mask_empty'\n | 'mask_too_long'\n | 'mask_unknown_token'\n | 'mask_missing_sequence'\n | 'mask_multiple_sequences'\n | 'mask_year_scope_needs_year'\n | 'mask_month_scope_needs_year_and_month'\n\nexport const MAX_MASK_LENGTH = 64\n\nconst TOKEN_RE = /\\{([A-Z_]+)(?::(\\d{1,2}))?\\}/g\nconst DATE_TOKENS = new Set(['YYYY', 'YY', 'MM', 'DD'])\nconst SEQUENCE_TOKENS: Record<string, SequenceScope> = {\n SEQUENCE_NUM: 'none',\n SEQUENCE_NUM_YEAR: 'year',\n SEQUENCE_NUM_MONTH: 'month',\n}\n\nfunction tokensOf(mask: string): Array<{ name: string; pad: number }> {\n return Array.from(mask.matchAll(TOKEN_RE), (m) => ({ name: m[1], pad: m[2] ? Number(m[2]) : 0 }))\n}\n\n/** Validation errors for a mask; empty array = valid. */\nexport function validateMask(mask: string): MaskError[] {\n const trimmed = (mask ?? '').trim()\n if (!trimmed) return ['mask_empty']\n const errors: MaskError[] = []\n if (trimmed.length > MAX_MASK_LENGTH) errors.push('mask_too_long')\n const tokens = tokensOf(trimmed)\n // Any brace not consumed by a known-shaped token is an unknown token.\n const stripped = trimmed.replace(TOKEN_RE, '')\n if (/[{}]/.test(stripped) || tokens.some((t) => !DATE_TOKENS.has(t.name) && !(t.name in SEQUENCE_TOKENS))) {\n errors.push('mask_unknown_token')\n }\n const sequences = tokens.filter((t) => t.name in SEQUENCE_TOKENS)\n if (sequences.length === 0) errors.push('mask_missing_sequence')\n if (sequences.length > 1) errors.push('mask_multiple_sequences')\n const hasYear = tokens.some((t) => t.name === 'YYYY' || t.name === 'YY')\n const hasMonth = tokens.some((t) => t.name === 'MM')\n const scope = sequences[0] ? SEQUENCE_TOKENS[sequences[0].name] : 'none'\n if (scope === 'year' && !hasYear) errors.push('mask_year_scope_needs_year')\n if (scope === 'month' && (!hasYear || !hasMonth)) errors.push('mask_month_scope_needs_year_and_month')\n return errors\n}\n\nexport function parseMask(mask: string): ParsedMask {\n const tokens = tokensOf(mask)\n const sequence = tokens.find((t) => t.name in SEQUENCE_TOKENS)\n return {\n scope: sequence ? SEQUENCE_TOKENS[sequence.name] : 'none',\n pad: sequence?.pad ?? 0,\n hasYear: tokens.some((t) => t.name === 'YYYY' || t.name === 'YY'),\n hasMonth: tokens.some((t) => t.name === 'MM'),\n }\n}\n\nfunction datePart(date: Date, part: 'YYYY' | 'YY' | 'MM' | 'DD'): string {\n const d = new Date(date)\n switch (part) {\n case 'YYYY':\n return String(d.getUTCFullYear())\n case 'YY':\n return String(d.getUTCFullYear()).slice(-2)\n case 'MM':\n return String(d.getUTCMonth() + 1).padStart(2, '0')\n case 'DD':\n return String(d.getUTCDate()).padStart(2, '0')\n }\n}\n\n/** Counter scope for a mask on a given issue date: '', 'YYYY' or 'YYYY-MM'. */\nexport function sequenceScopeKey(mask: string, issueDate: Date): string {\n const { scope } = parseMask(mask)\n if (scope === 'year') return datePart(issueDate, 'YYYY')\n if (scope === 'month') return `${datePart(issueDate, 'YYYY')}-${datePart(issueDate, 'MM')}`\n return ''\n}\n\nexport function renderNumber(mask: string, params: { seq: number; issueDate: Date }): string {\n return mask.replace(TOKEN_RE, (_match, name: string, pad?: string) => {\n if (name in SEQUENCE_TOKENS) return String(params.seq).padStart(pad ? Number(pad) : 0, '0')\n if (DATE_TOKENS.has(name)) return datePart(params.issueDate, name as 'YYYY' | 'YY' | 'MM' | 'DD')\n return _match\n })\n}\n\nexport function maskFor(masks: Partial<Record<string, string>> | null | undefined, series: DocumentSeries): string {\n const candidate = masks?.[series]\n return candidate && validateMask(candidate).length === 0 ? candidate : DEFAULT_NUMBER_MASKS[series]\n}\n\nfunction randomHex(length: number): string {\n let out = ''\n while (out.length < length) out += Math.floor(Math.random() * 0x100000000).toString(16).padStart(8, '0')\n return out.slice(0, length)\n}\n\n/** Temporary number for an unposted real draft (no sequence consumed). */\nexport function draftNumber(): string {\n return `DRAFT-${randomHex(8).toUpperCase()}`\n}\n\n/** Number for test-run drafts \u2014 never posted, never numbered. */\nexport function testNumber(): string {\n return `TEST-${Date.now().toString(16)}-${randomHex(8)}`\n}\n\nexport type NumberScope = { tenantId: string; organizationId: string }\n\n/**\n * Claims the next number of a series. MUST run inside the caller's\n * transaction (post command / receipt capture) so a rollback releases it.\n */\nexport async function claimDocumentNumber(\n em: EntityManager,\n scope: NumberScope,\n series: DocumentSeries,\n mask: string,\n issueDate: Date,\n): Promise<{ number: string; seq: number; scopeKey: string }> {\n const scopeKey = sequenceScopeKey(mask, issueDate)\n const now = new Date()\n const rows = (await em.execute(\n `INSERT INTO billing_document_sequences\n (tenant_id, organization_id, series, scope_key, next_value, created_at, updated_at)\n VALUES (?, ?, ?, ?, 2, ?, ?)\n ON CONFLICT (tenant_id, organization_id, series, scope_key)\n DO UPDATE SET next_value = billing_document_sequences.next_value + 1, updated_at = EXCLUDED.updated_at\n RETURNING next_value - 1 AS seq`,\n [scope.tenantId, scope.organizationId, series, scopeKey, now, now],\n )) as unknown as Array<{ seq: number | string }> | undefined\n const seq = Number(rows?.[0]?.seq)\n if (!Number.isFinite(seq) || seq < 1) {\n throw new Error(`Failed to claim a ${series} number`)\n }\n return { number: renderNumber(mask, { seq, issueDate }), seq, scopeKey }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const OPEN_STATUSES = ["posted", "partially_paid"];
|
|
2
|
+
const RECEIVABLE_DOCUMENT_TYPES = ["invoice", "debit_memo"];
|
|
3
|
+
function dayNumber(value) {
|
|
4
|
+
const date = typeof value === "string" ? /* @__PURE__ */ new Date(`${value.slice(0, 10)}T00:00:00Z`) : value;
|
|
5
|
+
return Math.floor(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()) / 864e5);
|
|
6
|
+
}
|
|
7
|
+
function isOverdue(doc, today) {
|
|
8
|
+
return daysOverdue(doc, today) > 0;
|
|
9
|
+
}
|
|
10
|
+
function daysOverdue(doc, today) {
|
|
11
|
+
if (!OPEN_STATUSES.includes(doc.status)) return 0;
|
|
12
|
+
if (doc.documentType && !RECEIVABLE_DOCUMENT_TYPES.includes(doc.documentType)) return 0;
|
|
13
|
+
if (!doc.dueDate) return 0;
|
|
14
|
+
const outstanding = typeof doc.outstanding === "number" ? doc.outstanding : Number.parseFloat(doc.outstanding ?? "0");
|
|
15
|
+
if (!(outstanding > 0)) return 0;
|
|
16
|
+
return Math.max(0, dayNumber(today) - dayNumber(doc.dueDate));
|
|
17
|
+
}
|
|
18
|
+
function isoDay(date) {
|
|
19
|
+
return date.toISOString().slice(0, 10);
|
|
20
|
+
}
|
|
21
|
+
function overdueSql(alias) {
|
|
22
|
+
return `(${alias}.status IN ('posted', 'partially_paid') AND ${alias}.document_type IN ('invoice', 'debit_memo') AND ${alias}.due_date < ?::date AND ${alias}.outstanding > 0)`;
|
|
23
|
+
}
|
|
24
|
+
function daysOverdueSql(alias) {
|
|
25
|
+
return `(CASE WHEN ${overdueSql(alias)} THEN (?::date - ${alias}.due_date) ELSE 0 END)`;
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
OPEN_STATUSES,
|
|
29
|
+
RECEIVABLE_DOCUMENT_TYPES,
|
|
30
|
+
daysOverdue,
|
|
31
|
+
daysOverdueSql,
|
|
32
|
+
isOverdue,
|
|
33
|
+
isoDay,
|
|
34
|
+
overdueSql
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=overdue.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/overdue.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Overdue is computed, never stored (Spec B, B-D6):\n *\n * status \u2208 {posted, partially_paid} AND due_date < today AND outstanding > 0\n *\n * Receivable documents only (invoices and debit memos). One definition shared\n * by the in-memory predicate (UI / tests) and the SQL fragment (invoice list,\n * account summary), so the two can never disagree. No worker until dunning.\n */\n\nexport const OPEN_STATUSES = ['posted', 'partially_paid'] as const\nexport const RECEIVABLE_DOCUMENT_TYPES = ['invoice', 'debit_memo'] as const\n\nexport type OverdueCandidate = {\n status: string\n documentType?: string | null\n dueDate: Date | string | null | undefined\n outstanding: string | number | null | undefined\n}\n\nfunction dayNumber(value: Date | string): number {\n const date = typeof value === 'string' ? new Date(`${value.slice(0, 10)}T00:00:00Z`) : value\n return Math.floor(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()) / 86_400_000)\n}\n\nexport function isOverdue(doc: OverdueCandidate, today: Date): boolean {\n return daysOverdue(doc, today) > 0\n}\n\n/** Whole days past the due date; 0 when not overdue. */\nexport function daysOverdue(doc: OverdueCandidate, today: Date): number {\n if (!(OPEN_STATUSES as readonly string[]).includes(doc.status)) return 0\n if (doc.documentType && !(RECEIVABLE_DOCUMENT_TYPES as readonly string[]).includes(doc.documentType)) return 0\n if (!doc.dueDate) return 0\n const outstanding = typeof doc.outstanding === 'number' ? doc.outstanding : Number.parseFloat(doc.outstanding ?? '0')\n if (!(outstanding > 0)) return 0\n return Math.max(0, dayNumber(today) - dayNumber(doc.dueDate))\n}\n\n/** `YYYY-MM-DD` of a date in UTC \u2014 the `today` parameter for SQL. */\nexport function isoDay(date: Date): string {\n return date.toISOString().slice(0, 10)\n}\n\n/**\n * SQL predicate for overdue documents on alias `alias`. Bind one parameter:\n * today as `YYYY-MM-DD`.\n */\nexport function overdueSql(alias: string): string {\n return (\n `(${alias}.status IN ('posted', 'partially_paid')` +\n ` AND ${alias}.document_type IN ('invoice', 'debit_memo')` +\n ` AND ${alias}.due_date < ?::date AND ${alias}.outstanding > 0)`\n )\n}\n\n/** SQL expression for days overdue (0 when not overdue). Bind today twice. */\nexport function daysOverdueSql(alias: string): string {\n return `(CASE WHEN ${overdueSql(alias)} THEN (?::date - ${alias}.due_date) ELSE 0 END)`\n}\n"],
|
|
5
|
+
"mappings": "AAUO,MAAM,gBAAgB,CAAC,UAAU,gBAAgB;AACjD,MAAM,4BAA4B,CAAC,WAAW,YAAY;AASjE,SAAS,UAAU,OAA8B;AAC/C,QAAM,OAAO,OAAO,UAAU,WAAW,oBAAI,KAAK,GAAG,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,IAAI;AACvF,SAAO,KAAK,MAAM,KAAK,IAAI,KAAK,eAAe,GAAG,KAAK,YAAY,GAAG,KAAK,WAAW,CAAC,IAAI,KAAU;AACvG;AAEO,SAAS,UAAU,KAAuB,OAAsB;AACrE,SAAO,YAAY,KAAK,KAAK,IAAI;AACnC;AAGO,SAAS,YAAY,KAAuB,OAAqB;AACtE,MAAI,CAAE,cAAoC,SAAS,IAAI,MAAM,EAAG,QAAO;AACvE,MAAI,IAAI,gBAAgB,CAAE,0BAAgD,SAAS,IAAI,YAAY,EAAG,QAAO;AAC7G,MAAI,CAAC,IAAI,QAAS,QAAO;AACzB,QAAM,cAAc,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc,OAAO,WAAW,IAAI,eAAe,GAAG;AACpH,MAAI,EAAE,cAAc,GAAI,QAAO;AAC/B,SAAO,KAAK,IAAI,GAAG,UAAU,KAAK,IAAI,UAAU,IAAI,OAAO,CAAC;AAC9D;AAGO,SAAS,OAAO,MAAoB;AACzC,SAAO,KAAK,YAAY,EAAE,MAAM,GAAG,EAAE;AACvC;AAMO,SAAS,WAAW,OAAuB;AAChD,SACE,IAAI,KAAK,+CACD,KAAK,mDACL,KAAK,2BAA2B,KAAK;AAEjD;AAGO,SAAS,eAAe,OAAuB;AACpD,SAAO,cAAc,WAAW,KAAK,CAAC,oBAAoB,KAAK;AACjE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { PAYMENT_IMPORT_MAX_ROWS } from "../data/validators.js";
|
|
4
|
+
const PAYMENT_CSV_HEADER = ["date", "amount", "currency", "reference", "payer_name", "payer_account"];
|
|
5
|
+
const rowSchema = z.object({
|
|
6
|
+
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "date must be YYYY-MM-DD").refine((value) => !Number.isNaN((/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).getTime()), "date is not a valid day"),
|
|
7
|
+
amount: z.string().regex(/^\d+(\.\d{1,4})?$/, 'amount must be a positive number with "." as decimal separator').refine((value) => Number.parseFloat(value) > 0, "amount must be greater than 0"),
|
|
8
|
+
currency: z.string().regex(/^[A-Za-z]{3}$/, "currency must be a 3-letter code"),
|
|
9
|
+
reference: z.string().max(500, "reference is longer than 500 characters"),
|
|
10
|
+
payer_name: z.string().max(255),
|
|
11
|
+
payer_account: z.string().max(64)
|
|
12
|
+
});
|
|
13
|
+
function splitCsv(text) {
|
|
14
|
+
const records = [];
|
|
15
|
+
let fields = [];
|
|
16
|
+
let field = "";
|
|
17
|
+
let inQuotes = false;
|
|
18
|
+
let line = 1;
|
|
19
|
+
let recordLine = 1;
|
|
20
|
+
const pushRecord = () => {
|
|
21
|
+
fields.push(field);
|
|
22
|
+
if (!(fields.length === 1 && fields[0].trim() === "")) records.push({ line: recordLine, fields });
|
|
23
|
+
fields = [];
|
|
24
|
+
field = "";
|
|
25
|
+
};
|
|
26
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
27
|
+
const char = text[i];
|
|
28
|
+
if (inQuotes) {
|
|
29
|
+
if (char === '"') {
|
|
30
|
+
if (text[i + 1] === '"') {
|
|
31
|
+
field += '"';
|
|
32
|
+
i += 1;
|
|
33
|
+
} else {
|
|
34
|
+
inQuotes = false;
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
if (char === "\n") line += 1;
|
|
38
|
+
field += char;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (char === '"' && field.trim() === "") {
|
|
43
|
+
field = "";
|
|
44
|
+
inQuotes = true;
|
|
45
|
+
} else if (char === ",") {
|
|
46
|
+
fields.push(field);
|
|
47
|
+
field = "";
|
|
48
|
+
} else if (char === "\n" || char === "\r") {
|
|
49
|
+
if (char === "\r" && text[i + 1] === "\n") i += 1;
|
|
50
|
+
pushRecord();
|
|
51
|
+
line += 1;
|
|
52
|
+
recordLine = line;
|
|
53
|
+
} else {
|
|
54
|
+
field += char;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (field !== "" || fields.length > 0) pushRecord();
|
|
58
|
+
return records;
|
|
59
|
+
}
|
|
60
|
+
function parsePaymentCsv(text) {
|
|
61
|
+
const records = splitCsv(text.replace(/^/, ""));
|
|
62
|
+
if (records.length === 0) return { rows: [], errors: [], fatal: "The file is empty" };
|
|
63
|
+
const header = records[0].fields.map((f) => f.trim().toLowerCase());
|
|
64
|
+
if (header.join(",") !== PAYMENT_CSV_HEADER.join(",")) {
|
|
65
|
+
return { rows: [], errors: [], fatal: `The header must be: ${PAYMENT_CSV_HEADER.join(",")}` };
|
|
66
|
+
}
|
|
67
|
+
const data = records.slice(1);
|
|
68
|
+
if (data.length > PAYMENT_IMPORT_MAX_ROWS) {
|
|
69
|
+
return { rows: [], errors: [], fatal: `The file has more than ${PAYMENT_IMPORT_MAX_ROWS} rows` };
|
|
70
|
+
}
|
|
71
|
+
const rows = [];
|
|
72
|
+
const errors = [];
|
|
73
|
+
for (const record of data) {
|
|
74
|
+
if (record.fields.length !== PAYMENT_CSV_HEADER.length) {
|
|
75
|
+
errors.push({ row: record.line, message: `expected ${PAYMENT_CSV_HEADER.length} columns, got ${record.fields.length}` });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const raw = Object.fromEntries(PAYMENT_CSV_HEADER.map((key, i) => [key, record.fields[i].trim()]));
|
|
79
|
+
const parsed = rowSchema.safeParse(raw);
|
|
80
|
+
if (!parsed.success) {
|
|
81
|
+
errors.push({ row: record.line, message: parsed.error.issues.map((issue) => issue.message).join("; ") });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
rows.push({
|
|
85
|
+
line: record.line,
|
|
86
|
+
receivedAt: parsed.data.date,
|
|
87
|
+
amount: Number.parseFloat(parsed.data.amount).toFixed(4),
|
|
88
|
+
currency: parsed.data.currency.toUpperCase(),
|
|
89
|
+
reference: parsed.data.reference || null,
|
|
90
|
+
payerName: parsed.data.payer_name || null,
|
|
91
|
+
payerAccount: parsed.data.payer_account ? parsed.data.payer_account.replace(/\s+/g, "").toUpperCase() : null
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return { rows, errors, fatal: null };
|
|
95
|
+
}
|
|
96
|
+
function paymentSourceRef(scope, row, occurrence) {
|
|
97
|
+
const key = [
|
|
98
|
+
scope.tenantId,
|
|
99
|
+
scope.organizationId,
|
|
100
|
+
row.receivedAt,
|
|
101
|
+
row.amount,
|
|
102
|
+
row.currency,
|
|
103
|
+
row.reference ?? "",
|
|
104
|
+
row.payerAccount ?? "",
|
|
105
|
+
String(occurrence)
|
|
106
|
+
].join("|");
|
|
107
|
+
return `csv:${createHash("sha256").update(key).digest("hex")}`;
|
|
108
|
+
}
|
|
109
|
+
function sourceRefsForRows(scope, rows) {
|
|
110
|
+
const seen = /* @__PURE__ */ new Map();
|
|
111
|
+
return rows.map((row) => {
|
|
112
|
+
const identity = [row.receivedAt, row.amount, row.currency, row.reference ?? "", row.payerAccount ?? ""].join("|");
|
|
113
|
+
const occurrence = (seen.get(identity) ?? 0) + 1;
|
|
114
|
+
seen.set(identity, occurrence);
|
|
115
|
+
return paymentSourceRef(scope, row, occurrence);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export {
|
|
119
|
+
PAYMENT_CSV_HEADER,
|
|
120
|
+
parsePaymentCsv,
|
|
121
|
+
paymentSourceRef,
|
|
122
|
+
sourceRefsForRows,
|
|
123
|
+
splitCsv
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=paymentCsv.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/paymentCsv.ts"],
|
|
4
|
+
"sourcesContent": ["import { createHash } from 'node:crypto'\nimport { z } from 'zod'\nimport { PAYMENT_IMPORT_MAX_ROWS } from '../data/validators'\n\n/**\n * Bank payment CSV parser (Spec C, demo-grade fixed format). Pure.\n *\n * date,amount,currency,reference,payer_name,payer_account\n * 2026-11-20,123.00,PLN,\"Payment for INV/2026/11/0003\",ACME Sp. z o.o.,PL61\u2026\n *\n * UTF-8 (BOM tolerated), comma separated, RFC-4180 quoting (`\"\"` escapes,\n * commas and newlines inside quotes), header required, `.` decimal\n * separator, amount > 0 (refunds are out of scope), \u2264 5,000 data rows.\n * Invalid rows are reported with their line number and skipped; they never\n * abort the file.\n */\n\nexport const PAYMENT_CSV_HEADER = ['date', 'amount', 'currency', 'reference', 'payer_name', 'payer_account'] as const\n\nexport type PaymentCsvRow = {\n line: number\n receivedAt: string\n amount: string\n currency: string\n reference: string | null\n payerName: string | null\n payerAccount: string | null\n}\n\nexport type PaymentCsvResult = {\n rows: PaymentCsvRow[]\n errors: Array<{ row: number; message: string }>\n fatal: string | null\n}\n\nconst rowSchema = z.object({\n date: z\n .string()\n .regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'date must be YYYY-MM-DD')\n .refine((value) => !Number.isNaN(new Date(`${value}T00:00:00Z`).getTime()), 'date is not a valid day'),\n amount: z\n .string()\n .regex(/^\\d+(\\.\\d{1,4})?$/, 'amount must be a positive number with \".\" as decimal separator')\n .refine((value) => Number.parseFloat(value) > 0, 'amount must be greater than 0'),\n currency: z.string().regex(/^[A-Za-z]{3}$/, 'currency must be a 3-letter code'),\n reference: z.string().max(500, 'reference is longer than 500 characters'),\n payer_name: z.string().max(255),\n payer_account: z.string().max(64),\n})\n\n/** Splits CSV text into records of fields (RFC 4180). Returns the 1-based start line of each record. */\nexport function splitCsv(text: string): Array<{ line: number; fields: string[] }> {\n const records: Array<{ line: number; fields: string[] }> = []\n let fields: string[] = []\n let field = ''\n let inQuotes = false\n let line = 1\n let recordLine = 1\n const pushRecord = () => {\n fields.push(field)\n if (!(fields.length === 1 && fields[0].trim() === '')) records.push({ line: recordLine, fields })\n fields = []\n field = ''\n }\n for (let i = 0; i < text.length; i += 1) {\n const char = text[i]\n if (inQuotes) {\n if (char === '\"') {\n if (text[i + 1] === '\"') {\n field += '\"'\n i += 1\n } else {\n inQuotes = false\n }\n } else {\n if (char === '\\n') line += 1\n field += char\n }\n continue\n }\n if (char === '\"' && field.trim() === '') {\n field = ''\n inQuotes = true\n } else if (char === ',') {\n fields.push(field)\n field = ''\n } else if (char === '\\n' || char === '\\r') {\n if (char === '\\r' && text[i + 1] === '\\n') i += 1\n pushRecord()\n line += 1\n recordLine = line\n } else {\n field += char\n }\n }\n if (field !== '' || fields.length > 0) pushRecord()\n return records\n}\n\nexport function parsePaymentCsv(text: string): PaymentCsvResult {\n const records = splitCsv(text.replace(/^\uFEFF/, ''))\n if (records.length === 0) return { rows: [], errors: [], fatal: 'The file is empty' }\n const header = records[0].fields.map((f) => f.trim().toLowerCase())\n if (header.join(',') !== PAYMENT_CSV_HEADER.join(',')) {\n return { rows: [], errors: [], fatal: `The header must be: ${PAYMENT_CSV_HEADER.join(',')}` }\n }\n const data = records.slice(1)\n if (data.length > PAYMENT_IMPORT_MAX_ROWS) {\n return { rows: [], errors: [], fatal: `The file has more than ${PAYMENT_IMPORT_MAX_ROWS} rows` }\n }\n const rows: PaymentCsvRow[] = []\n const errors: PaymentCsvResult['errors'] = []\n for (const record of data) {\n if (record.fields.length !== PAYMENT_CSV_HEADER.length) {\n errors.push({ row: record.line, message: `expected ${PAYMENT_CSV_HEADER.length} columns, got ${record.fields.length}` })\n continue\n }\n const raw = Object.fromEntries(PAYMENT_CSV_HEADER.map((key, i) => [key, record.fields[i].trim()]))\n const parsed = rowSchema.safeParse(raw)\n if (!parsed.success) {\n errors.push({ row: record.line, message: parsed.error.issues.map((issue) => issue.message).join('; ') })\n continue\n }\n rows.push({\n line: record.line,\n receivedAt: parsed.data.date,\n amount: Number.parseFloat(parsed.data.amount).toFixed(4),\n currency: parsed.data.currency.toUpperCase(),\n reference: parsed.data.reference || null,\n payerName: parsed.data.payer_name || null,\n payerAccount: parsed.data.payer_account ? parsed.data.payer_account.replace(/\\s+/g, '').toUpperCase() : null,\n })\n }\n return { rows, errors, fatal: null }\n}\n\n/**\n * Duplicate guard (P-D7): the same bank line uploaded again yields the same\n * key; identical lines inside one file differ by their occurrence index.\n */\nexport function paymentSourceRef(\n scope: { tenantId: string; organizationId: string },\n row: Pick<PaymentCsvRow, 'receivedAt' | 'amount' | 'currency' | 'reference' | 'payerAccount'>,\n occurrence: number,\n): string {\n const key = [\n scope.tenantId,\n scope.organizationId,\n row.receivedAt,\n row.amount,\n row.currency,\n row.reference ?? '',\n row.payerAccount ?? '',\n String(occurrence),\n ].join('|')\n return `csv:${createHash('sha256').update(key).digest('hex')}`\n}\n\n/** Source refs for all rows of a file, counting identical lines (occurrence index). */\nexport function sourceRefsForRows(\n scope: { tenantId: string; organizationId: string },\n rows: PaymentCsvRow[],\n): string[] {\n const seen = new Map<string, number>()\n return rows.map((row) => {\n const identity = [row.receivedAt, row.amount, row.currency, row.reference ?? '', row.payerAccount ?? ''].join('|')\n const occurrence = (seen.get(identity) ?? 0) + 1\n seen.set(identity, occurrence)\n return paymentSourceRef(scope, row, occurrence)\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const PAYMENT_GATEWAY_SERVICE = "paymentGatewayService";
|
|
2
|
+
function isPaymentGatewayAvailable(container) {
|
|
3
|
+
return typeof container.hasRegistration === "function" && container.hasRegistration(PAYMENT_GATEWAY_SERVICE);
|
|
4
|
+
}
|
|
5
|
+
function resolvePaymentGateway(container) {
|
|
6
|
+
if (!isPaymentGatewayAvailable(container)) return null;
|
|
7
|
+
const service = container.resolve(PAYMENT_GATEWAY_SERVICE);
|
|
8
|
+
return service && typeof service.createPaymentSession === "function" ? service : null;
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
PAYMENT_GATEWAY_SERVICE,
|
|
12
|
+
isPaymentGatewayAvailable,
|
|
13
|
+
resolvePaymentGateway
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=paymentGateway.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/lib/paymentGateway.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Optional payment-gateway integration (Spec 0, E-D1).\n *\n * Billing no longer requires the `payment_gateways` module. When it is\n * installed, online top-ups open a checkout session through its\n * `paymentGatewayService`; when it isn't, top-ups via the gateway are\n * unavailable (501) and the UI hides the \"Top up\" button. Bank-transfer\n * top-ups are recorded through payments (billing Spec C).\n *\n * The structural type below covers only what billing calls, so billing never\n * deep-imports the gateway module's types.\n */\n\nexport type GatewaySessionRequest = {\n providerKey: string\n paymentId: string\n amount: number\n currencyCode: string\n captureMethod: 'automatic' | 'manual'\n successUrl?: string\n cancelUrl?: string\n metadata?: Record<string, unknown>\n organizationId: string\n tenantId: string\n}\n\nexport type GatewaySessionResult = {\n transaction: { id: string }\n session: { redirectUrl?: string | null; clientSecret?: string | null; sessionId?: string | null }\n}\n\nexport type TopupPaymentGateway = {\n createPaymentSession(request: GatewaySessionRequest): Promise<GatewaySessionResult>\n}\n\ntype ContainerLike = {\n hasRegistration?: (name: string) => boolean\n resolve: (name: string) => unknown\n}\n\nexport const PAYMENT_GATEWAY_SERVICE = 'paymentGatewayService'\n\nexport function isPaymentGatewayAvailable(container: ContainerLike): boolean {\n return typeof container.hasRegistration === 'function' && container.hasRegistration(PAYMENT_GATEWAY_SERVICE)\n}\n\n/** The gateway service, or null when the `payment_gateways` module isn't installed. */\nexport function resolvePaymentGateway(container: ContainerLike): TopupPaymentGateway | null {\n if (!isPaymentGatewayAvailable(container)) return null\n const service = container.resolve(PAYMENT_GATEWAY_SERVICE) as Partial<TopupPaymentGateway> | null\n return service && typeof service.createPaymentSession === 'function' ? (service as TopupPaymentGateway) : null\n}\n"],
|
|
5
|
+
"mappings": "AAwCO,MAAM,0BAA0B;AAEhC,SAAS,0BAA0B,WAAmC;AAC3E,SAAO,OAAO,UAAU,oBAAoB,cAAc,UAAU,gBAAgB,uBAAuB;AAC7G;AAGO,SAAS,sBAAsB,WAAsD;AAC1F,MAAI,CAAC,0BAA0B,SAAS,EAAG,QAAO;AAClD,QAAM,UAAU,UAAU,QAAQ,uBAAuB;AACzD,SAAO,WAAW,OAAO,QAAQ,yBAAyB,aAAc,UAAkC;AAC5G;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|