@dainamite/billing 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/dist/modules/billing/acl.js +13 -1
- package/dist/modules/billing/acl.js.map +2 -2
- package/dist/modules/billing/api/accounts/[id]/summary/route.js +52 -0
- package/dist/modules/billing/api/accounts/[id]/summary/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +47 -4
- package/dist/modules/billing/api/accounts/route.js.map +2 -2
- package/dist/modules/billing/api/export/account/[id]/route.js +11 -7
- package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/[id]/memos/route.js +54 -0
- package/dist/modules/billing/api/invoices/[id]/memos/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/pdf/route.js +70 -0
- package/dist/modules/billing/api/invoices/[id]/pdf/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/route.js +44 -24
- package/dist/modules/billing/api/invoices/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/[id]/send/route.js +107 -0
- package/dist/modules/billing/api/invoices/[id]/send/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/add-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/add-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/delete-draft/route.js +33 -0
- package/dist/modules/billing/api/invoices/delete-draft/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/edit-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/post/route.js +2 -2
- package/dist/modules/billing/api/invoices/post/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/remove-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/remove-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/route.js +45 -46
- package/dist/modules/billing/api/invoices/route.js.map +2 -2
- package/dist/modules/billing/api/items/bulk/route.js +1 -1
- package/dist/modules/billing/api/items/bulk/route.js.map +2 -2
- package/dist/modules/billing/api/items/change-price/route.js +44 -0
- package/dist/modules/billing/api/items/change-price/route.js.map +7 -0
- package/dist/modules/billing/api/items/route.js +19 -7
- package/dist/modules/billing/api/items/route.js.map +2 -2
- package/dist/modules/billing/api/payment-imports/[id]/route.js +49 -0
- package/dist/modules/billing/api/payment-imports/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/payment-imports/route.js +57 -0
- package/dist/modules/billing/api/payment-imports/route.js.map +7 -0
- package/dist/modules/billing/api/payments/[id]/route.js +60 -0
- package/dist/modules/billing/api/payments/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/payments/allocate/route.js +35 -0
- package/dist/modules/billing/api/payments/allocate/route.js.map +7 -0
- package/dist/modules/billing/api/payments/assign/route.js +36 -0
- package/dist/modules/billing/api/payments/assign/route.js.map +7 -0
- package/dist/modules/billing/api/payments/convert-to-topup/route.js +35 -0
- package/dist/modules/billing/api/payments/convert-to-topup/route.js.map +7 -0
- package/dist/modules/billing/api/payments/ignore/route.js +35 -0
- package/dist/modules/billing/api/payments/ignore/route.js.map +7 -0
- package/dist/modules/billing/api/payments/import/route.js +63 -0
- package/dist/modules/billing/api/payments/import/route.js.map +7 -0
- package/dist/modules/billing/api/payments/restore/route.js +35 -0
- package/dist/modules/billing/api/payments/restore/route.js.map +7 -0
- package/dist/modules/billing/api/payments/route.js +114 -0
- package/dist/modules/billing/api/payments/route.js.map +7 -0
- package/dist/modules/billing/api/payments/unallocate/route.js +35 -0
- package/dist/modules/billing/api/payments/unallocate/route.js.map +7 -0
- package/dist/modules/billing/api/payments/unassign/route.js +35 -0
- package/dist/modules/billing/api/payments/unassign/route.js.map +7 -0
- package/dist/modules/billing/api/run-outcomes/route.js +43 -6
- package/dist/modules/billing/api/run-outcomes/route.js.map +2 -2
- package/dist/modules/billing/api/runs/[id]/post-drafts/route.js +61 -0
- package/dist/modules/billing/api/runs/[id]/post-drafts/route.js.map +7 -0
- package/dist/modules/billing/api/runs/[id]/route.js +53 -0
- package/dist/modules/billing/api/runs/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js +1 -1
- package/dist/modules/billing/api/runs/retry-failed/route.js.map +2 -2
- package/dist/modules/billing/api/runs/route.js +9 -3
- package/dist/modules/billing/api/runs/route.js.map +2 -2
- package/dist/modules/billing/api/settings/number-preview/route.js +42 -0
- package/dist/modules/billing/api/settings/number-preview/route.js.map +7 -0
- package/dist/modules/billing/api/settings/route.js +92 -0
- package/dist/modules/billing/api/settings/route.js.map +7 -0
- package/dist/modules/billing/api/statements/route.js +3 -0
- package/dist/modules/billing/api/statements/route.js.map +2 -2
- package/dist/modules/billing/api/test-invoices/wipe/route.js +2 -2
- package/dist/modules/billing/api/test-invoices/wipe/route.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +121 -232
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/create/page.js +6 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/page.js +63 -4
- package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js +297 -65
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +3 -3
- package/dist/modules/billing/backend/billing/invoices/page.js +6 -172
- package/dist/modules/billing/backend/billing/invoices/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/[id]/page.js +51 -7
- package/dist/modules/billing/backend/billing/items/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/create/page.js +9 -6
- package/dist/modules/billing/backend/billing/items/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/page.js +8 -2
- package/dist/modules/billing/backend/billing/items/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/payments/[id]/page.js +297 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js +74 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/page.js +58 -0
- package/dist/modules/billing/backend/billing/payments/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/payments/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js +49 -14
- package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/runs/page.js +58 -7
- package/dist/modules/billing/backend/billing/runs/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/[id]/page.js +61 -6
- package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/page.js +1 -1
- package/dist/modules/billing/backend/billing/statements/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/topups/page.js +1 -1
- package/dist/modules/billing/backend/billing/topups/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/transactions/page.js +1 -1
- package/dist/modules/billing/backend/billing/transactions/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/usage/create/page.js +3 -2
- package/dist/modules/billing/backend/billing/usage/create/page.js.map +2 -2
- package/dist/modules/billing/backend/config/billing/page.js +472 -0
- package/dist/modules/billing/backend/config/billing/page.js.map +7 -0
- package/dist/modules/billing/backend/config/billing/page.meta.js +19 -0
- package/dist/modules/billing/backend/config/billing/page.meta.js.map +7 -0
- package/dist/modules/billing/cli.js +22 -1
- package/dist/modules/billing/cli.js.map +2 -2
- package/dist/modules/billing/commands/accounts.js +6 -0
- package/dist/modules/billing/commands/accounts.js.map +2 -2
- package/dist/modules/billing/commands/index.js +3 -0
- package/dist/modules/billing/commands/index.js.map +2 -2
- package/dist/modules/billing/commands/invoices.js +269 -167
- package/dist/modules/billing/commands/invoices.js.map +2 -2
- package/dist/modules/billing/commands/items.js +108 -0
- package/dist/modules/billing/commands/items.js.map +2 -2
- package/dist/modules/billing/commands/memos.js +179 -0
- package/dist/modules/billing/commands/memos.js.map +7 -0
- package/dist/modules/billing/commands/payments.js +379 -0
- package/dist/modules/billing/commands/payments.js.map +7 -0
- package/dist/modules/billing/commands/runs.js +151 -4
- package/dist/modules/billing/commands/runs.js.map +2 -2
- package/dist/modules/billing/commands/settings.js +59 -0
- package/dist/modules/billing/commands/settings.js.map +7 -0
- package/dist/modules/billing/commands/topups.js +9 -2
- package/dist/modules/billing/commands/topups.js.map +2 -2
- package/dist/modules/billing/commands/transactions.js +2 -2
- package/dist/modules/billing/commands/transactions.js.map +2 -2
- package/dist/modules/billing/components/AccountForm.js +149 -42
- package/dist/modules/billing/components/AccountForm.js.map +2 -2
- package/dist/modules/billing/components/AccountInvoicesSection.js +64 -0
- package/dist/modules/billing/components/AccountInvoicesSection.js.map +7 -0
- package/dist/modules/billing/components/AccountPaymentsSection.js +43 -0
- package/dist/modules/billing/components/AccountPaymentsSection.js.map +7 -0
- package/dist/modules/billing/components/AccountPicker.js +56 -0
- package/dist/modules/billing/components/AccountPicker.js.map +7 -0
- package/dist/modules/billing/components/AllocatePaymentDialog.js +134 -0
- package/dist/modules/billing/components/AllocatePaymentDialog.js.map +7 -0
- package/dist/modules/billing/components/BillingDataTable.js +21 -0
- package/dist/modules/billing/components/BillingDataTable.js.map +7 -0
- package/dist/modules/billing/components/BillingSettingsLink.js +19 -0
- package/dist/modules/billing/components/BillingSettingsLink.js.map +7 -0
- package/dist/modules/billing/components/ChangeItemPriceDialog.js +158 -0
- package/dist/modules/billing/components/ChangeItemPriceDialog.js.map +7 -0
- package/dist/modules/billing/components/CreateMemoDialog.js +200 -0
- package/dist/modules/billing/components/CreateMemoDialog.js.map +7 -0
- package/dist/modules/billing/components/CustomerLabel.js +33 -0
- package/dist/modules/billing/components/CustomerLabel.js.map +7 -0
- package/dist/modules/billing/components/InvoicesTable.js +270 -0
- package/dist/modules/billing/components/InvoicesTable.js.map +7 -0
- package/dist/modules/billing/components/ItemForm.js +176 -44
- package/dist/modules/billing/components/ItemForm.js.map +2 -2
- package/dist/modules/billing/components/ItemPriceField.js +46 -0
- package/dist/modules/billing/components/ItemPriceField.js.map +7 -0
- package/dist/modules/billing/components/LineFormDialog.js +25 -2
- package/dist/modules/billing/components/LineFormDialog.js.map +2 -2
- package/dist/modules/billing/components/ManualPaymentDialog.js +122 -0
- package/dist/modules/billing/components/ManualPaymentDialog.js.map +7 -0
- package/dist/modules/billing/components/MoneyInput.js +14 -0
- package/dist/modules/billing/components/MoneyInput.js.map +7 -0
- package/dist/modules/billing/components/PaymentUploadDialog.js +115 -0
- package/dist/modules/billing/components/PaymentUploadDialog.js.map +7 -0
- package/dist/modules/billing/components/PaymentsTable.js +171 -0
- package/dist/modules/billing/components/PaymentsTable.js.map +7 -0
- package/dist/modules/billing/components/PostRunDraftsButton.js +105 -0
- package/dist/modules/billing/components/PostRunDraftsButton.js.map +7 -0
- package/dist/modules/billing/components/PrepaidPanel.js +14 -4
- package/dist/modules/billing/components/PrepaidPanel.js.map +2 -2
- package/dist/modules/billing/components/RunCyclesSection.js +209 -0
- package/dist/modules/billing/components/RunCyclesSection.js.map +7 -0
- package/dist/modules/billing/components/TestRunActions.js +127 -0
- package/dist/modules/billing/components/TestRunActions.js.map +7 -0
- package/dist/modules/billing/components/account/AccountItemsTab.js +157 -0
- package/dist/modules/billing/components/account/AccountItemsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountRunBillingDialog.js +122 -0
- package/dist/modules/billing/components/account/AccountRunBillingDialog.js.map +7 -0
- package/dist/modules/billing/components/account/AccountRunsTab.js +152 -0
- package/dist/modules/billing/components/account/AccountRunsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountStatementsTab.js +98 -0
- package/dist/modules/billing/components/account/AccountStatementsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountSummaryCards.js +125 -0
- package/dist/modules/billing/components/account/AccountSummaryCards.js.map +7 -0
- package/dist/modules/billing/components/account/AccountUsageTab.js +91 -0
- package/dist/modules/billing/components/account/AccountUsageTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountView.js +73 -0
- package/dist/modules/billing/components/account/AccountView.js.map +7 -0
- package/dist/modules/billing/components/account/accountRow.js +39 -0
- package/dist/modules/billing/components/account/accountRow.js.map +7 -0
- package/dist/modules/billing/components/customerNames.js +42 -0
- package/dist/modules/billing/components/customerNames.js.map +7 -0
- package/dist/modules/billing/data/entities.js +560 -4
- package/dist/modules/billing/data/entities.js.map +2 -2
- package/dist/modules/billing/data/entityIds.js +10 -1
- package/dist/modules/billing/data/entityIds.js.map +2 -2
- package/dist/modules/billing/data/validators.js +269 -15
- package/dist/modules/billing/data/validators.js.map +2 -2
- package/dist/modules/billing/events.js +50 -0
- package/dist/modules/billing/events.js.map +2 -2
- package/dist/modules/billing/i18n/de.json +880 -0
- package/dist/modules/billing/i18n/en.json +457 -21
- package/dist/modules/billing/i18n/es.json +880 -0
- package/dist/modules/billing/i18n/pl.json +458 -22
- package/dist/modules/billing/index.js +6 -2
- package/dist/modules/billing/index.js.map +2 -2
- package/dist/modules/billing/lib/accountFinancials.js +58 -0
- package/dist/modules/billing/lib/accountFinancials.js.map +7 -0
- package/dist/modules/billing/lib/accountLock.js +14 -0
- package/dist/modules/billing/lib/accountLock.js.map +7 -0
- package/dist/modules/billing/lib/accountStatus.js +9 -7
- package/dist/modules/billing/lib/accountStatus.js.map +2 -2
- package/dist/modules/billing/lib/accountSummary.js +106 -0
- package/dist/modules/billing/lib/accountSummary.js.map +7 -0
- package/dist/modules/billing/lib/allocation.js +356 -0
- package/dist/modules/billing/lib/allocation.js.map +7 -0
- package/dist/modules/billing/lib/allocationEvents.js +22 -0
- package/dist/modules/billing/lib/allocationEvents.js.map +7 -0
- package/dist/modules/billing/lib/billPeriod.js +15 -1
- package/dist/modules/billing/lib/billPeriod.js.map +2 -2
- package/dist/modules/billing/lib/billRunEngine.js +237 -102
- package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
- package/dist/modules/billing/lib/billingSettings.js +116 -0
- package/dist/modules/billing/lib/billingSettings.js.map +7 -0
- package/dist/modules/billing/lib/commandRoute.js +81 -0
- package/dist/modules/billing/lib/commandRoute.js.map +7 -0
- package/dist/modules/billing/lib/customerLookup.js +97 -0
- package/dist/modules/billing/lib/customerLookup.js.map +7 -0
- package/dist/modules/billing/lib/dateOnly.js +13 -0
- package/dist/modules/billing/lib/dateOnly.js.map +7 -0
- package/dist/modules/billing/lib/documentParties.js +16 -0
- package/dist/modules/billing/lib/documentParties.js.map +7 -0
- package/dist/modules/billing/lib/documentTypes.js +27 -0
- package/dist/modules/billing/lib/documentTypes.js.map +7 -0
- package/dist/modules/billing/lib/dueDate.js +18 -0
- package/dist/modules/billing/lib/dueDate.js.map +7 -0
- package/dist/modules/billing/lib/iban.js +29 -0
- package/dist/modules/billing/lib/iban.js.map +7 -0
- package/dist/modules/billing/lib/invoiceDelivery.js +133 -0
- package/dist/modules/billing/lib/invoiceDelivery.js.map +7 -0
- package/dist/modules/billing/lib/invoiceLanguages.js +28 -0
- package/dist/modules/billing/lib/invoiceLanguages.js.map +7 -0
- package/dist/modules/billing/lib/invoiceWriter.js +107 -99
- package/dist/modules/billing/lib/invoiceWriter.js.map +2 -2
- package/dist/modules/billing/lib/itemBillingState.js +27 -0
- package/dist/modules/billing/lib/itemBillingState.js.map +7 -0
- package/dist/modules/billing/lib/itemPriceChange.js +18 -0
- package/dist/modules/billing/lib/itemPriceChange.js.map +7 -0
- package/dist/modules/billing/lib/itemSelector.js.map +2 -2
- package/dist/modules/billing/lib/lineAmounts.js +32 -0
- package/dist/modules/billing/lib/lineAmounts.js.map +7 -0
- package/dist/modules/billing/lib/memoGuards.js +25 -0
- package/dist/modules/billing/lib/memoGuards.js.map +7 -0
- package/dist/modules/billing/lib/navigation.js +48 -0
- package/dist/modules/billing/lib/navigation.js.map +7 -0
- package/dist/modules/billing/lib/numbering.js +120 -0
- package/dist/modules/billing/lib/numbering.js.map +7 -0
- package/dist/modules/billing/lib/overdue.js +36 -0
- package/dist/modules/billing/lib/overdue.js.map +7 -0
- package/dist/modules/billing/lib/paymentCsv.js +125 -0
- package/dist/modules/billing/lib/paymentCsv.js.map +7 -0
- package/dist/modules/billing/lib/paymentGateway.js +15 -0
- package/dist/modules/billing/lib/paymentGateway.js.map +7 -0
- package/dist/modules/billing/lib/paymentImport.js +164 -0
- package/dist/modules/billing/lib/paymentImport.js.map +7 -0
- package/dist/modules/billing/lib/paymentMatcher.js +36 -0
- package/dist/modules/billing/lib/paymentMatcher.js.map +7 -0
- package/dist/modules/billing/lib/pdf/attachmentStore.js +46 -0
- package/dist/modules/billing/lib/pdf/attachmentStore.js.map +7 -0
- package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js +7 -0
- package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoiceModel.js +194 -0
- package/dist/modules/billing/lib/pdf/invoiceModel.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoicePdf.js +165 -0
- package/dist/modules/billing/lib/pdf/invoicePdf.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoiceTemplate.js +276 -0
- package/dist/modules/billing/lib/pdf/invoiceTemplate.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConfig.js +17 -25
- package/dist/modules/billing/lib/prepaidConfig.js.map +2 -2
- package/dist/modules/billing/lib/prepaidConsume.js +26 -7
- package/dist/modules/billing/lib/prepaidConsume.js.map +2 -2
- package/dist/modules/billing/lib/prepaidPeriodClose.js +277 -158
- package/dist/modules/billing/lib/prepaidPeriodClose.js.map +2 -2
- package/dist/modules/billing/lib/prepaidTaxPoint.js +37 -0
- package/dist/modules/billing/lib/prepaidTaxPoint.js.map +7 -0
- package/dist/modules/billing/lib/seeds/demo.js +51 -3
- package/dist/modules/billing/lib/seeds/demo.js.map +2 -2
- package/dist/modules/billing/lib/seeds/paymentsSample.js +93 -0
- package/dist/modules/billing/lib/seeds/paymentsSample.js.map +7 -0
- package/dist/modules/billing/lib/seeds/sellerSettings.js +41 -0
- package/dist/modules/billing/lib/seeds/sellerSettings.js.map +7 -0
- package/dist/modules/billing/lib/servicePeriods.js +121 -0
- package/dist/modules/billing/lib/servicePeriods.js.map +7 -0
- package/dist/modules/billing/lib/testRunCleanup.js +24 -0
- package/dist/modules/billing/lib/testRunCleanup.js.map +7 -0
- package/dist/modules/billing/lib/topupCapture.js +2 -2
- package/dist/modules/billing/lib/topupCapture.js.map +2 -2
- package/dist/modules/billing/lib/topupReceipt.js +58 -79
- package/dist/modules/billing/lib/topupReceipt.js.map +2 -2
- package/dist/modules/billing/lib/usageRater.js.map +1 -1
- package/dist/modules/billing/lib/usageRunner.js +14 -6
- package/dist/modules/billing/lib/usageRunner.js.map +2 -2
- package/dist/modules/billing/lib/vat.js +39 -0
- package/dist/modules/billing/lib/vat.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js +15 -0
- package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js +25 -0
- package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js +115 -0
- package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js +47 -0
- package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js +89 -0
- package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js +17 -0
- package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js +27 -0
- package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js +29 -0
- package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js.map +7 -0
- package/dist/modules/billing/setup.js +7 -184
- package/dist/modules/billing/setup.js.map +2 -2
- package/dist/modules/billing/subscribers/credit-memo-auto-send.js +16 -0
- package/dist/modules/billing/subscribers/credit-memo-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/debit-memo-apply-credit.js +16 -0
- package/dist/modules/billing/subscribers/debit-memo-apply-credit.js.map +7 -0
- package/dist/modules/billing/subscribers/debit-memo-auto-send.js +16 -0
- package/dist/modules/billing/subscribers/debit-memo-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/invoice-apply-credit.js +34 -0
- package/dist/modules/billing/subscribers/invoice-apply-credit.js.map +7 -0
- package/dist/modules/billing/subscribers/invoice-auto-send.js +30 -0
- package/dist/modules/billing/subscribers/invoice-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js +69 -33
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +2 -2
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js +4 -6
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +2 -2
- package/generated/entities/billing_account/index.ts +3 -0
- package/generated/entities/billing_allocation/index.ts +18 -0
- package/generated/entities/billing_document_sequence/index.ts +11 -0
- package/generated/entities/billing_invoice/index.ts +38 -0
- package/generated/entities/billing_invoice_line/index.ts +24 -0
- package/generated/entities/billing_item/index.ts +4 -0
- package/generated/entities/billing_payment/index.ts +25 -0
- package/generated/entities/billing_payment_import/index.ts +14 -0
- package/generated/entities/billing_run/index.ts +0 -1
- package/generated/entities/billing_run_outcome/index.ts +1 -0
- package/generated/entities/billing_settings/index.ts +26 -0
- package/generated/entities/billing_statement/index.ts +1 -0
- package/generated/entities.ids.generated.ts +7 -0
- package/package.json +5 -1
- package/src/modules/billing/__integration__/TC-BILL-004.spec.ts +171 -0
- package/src/modules/billing/__integration__/TC-BILL-005.spec.ts +134 -0
- package/src/modules/billing/__integration__/TC-BILL-006.spec.ts +101 -0
- package/src/modules/billing/__integration__/TC-BILL-007.spec.ts +140 -0
- package/src/modules/billing/__integration__/TC-BILL-008.spec.ts +108 -0
- package/src/modules/billing/__integration__/TC-BILL-009.spec.ts +113 -0
- package/src/modules/billing/__integration__/TC-BILL-010.spec.ts +118 -0
- package/src/modules/billing/__integration__/TC-BILL-011.spec.ts +142 -0
- package/src/modules/billing/__integration__/TC-BILL-012.spec.ts +115 -0
- package/src/modules/billing/__integration__/TC-BILL-013.spec.ts +118 -0
- package/src/modules/billing/__integration__/sellerSettings.ts +24 -0
- package/src/modules/billing/__tests__/commands/invoices.test.ts +436 -354
- package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +33 -0
- package/src/modules/billing/__tests__/commands/memos.test.ts +205 -0
- package/src/modules/billing/__tests__/commands/payments.test.ts +241 -0
- package/src/modules/billing/__tests__/commands/runsDelete.test.ts +146 -0
- package/src/modules/billing/__tests__/commands/runsPostDrafts.test.ts +106 -0
- package/src/modules/billing/__tests__/commands/topups.test.ts +11 -0
- package/src/modules/billing/__tests__/entityRegistry.test.ts +8 -1
- package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +28 -11
- package/src/modules/billing/__tests__/i18n/dictionaries.test.ts +64 -0
- package/src/modules/billing/__tests__/lib/accountFinancials.test.ts +53 -0
- package/src/modules/billing/__tests__/lib/accountSummary.test.ts +97 -0
- package/src/modules/billing/__tests__/lib/allocation.test.ts +319 -0
- package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +228 -17
- package/src/modules/billing/__tests__/lib/billingSettings.test.ts +109 -0
- package/src/modules/billing/__tests__/lib/customerLookup.test.ts +156 -0
- package/src/modules/billing/__tests__/lib/dueDate.test.ts +35 -0
- package/src/modules/billing/__tests__/lib/iban.test.ts +38 -0
- package/src/modules/billing/__tests__/lib/invoiceDelivery.test.ts +206 -0
- package/src/modules/billing/__tests__/lib/invoiceLanguages.test.ts +49 -0
- package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +212 -10
- package/src/modules/billing/__tests__/lib/itemBillingState.test.ts +42 -0
- package/src/modules/billing/__tests__/lib/itemPriceChange.test.ts +165 -0
- package/src/modules/billing/__tests__/lib/lineAmounts.test.ts +86 -0
- package/src/modules/billing/__tests__/lib/navigation.test.ts +71 -0
- package/src/modules/billing/__tests__/lib/numbering.test.ts +91 -0
- package/src/modules/billing/__tests__/lib/overdue.test.ts +36 -0
- package/src/modules/billing/__tests__/lib/paymentCsv.test.ts +148 -0
- package/src/modules/billing/__tests__/lib/paymentImport.test.ts +113 -0
- package/src/modules/billing/__tests__/lib/paymentsSample.test.ts +73 -0
- package/src/modules/billing/__tests__/lib/pdf/invoicePdf.test.ts +269 -0
- package/src/modules/billing/__tests__/lib/pdf/invoicePdfService.test.ts +147 -0
- package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +374 -98
- package/src/modules/billing/__tests__/lib/prepaidTaxPoint.test.ts +118 -0
- package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +53 -2
- package/src/modules/billing/__tests__/lib/sellerSettings.test.ts +28 -0
- package/src/modules/billing/__tests__/lib/servicePeriods.test.ts +190 -0
- package/src/modules/billing/__tests__/lib/topupCapture.test.ts +6 -28
- package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +21 -52
- package/src/modules/billing/__tests__/lib/vat.test.ts +51 -0
- package/src/modules/billing/__tests__/package-isolation.test.ts +22 -8
- package/src/modules/billing/__tests__/setup.test.ts +13 -4
- package/src/modules/billing/__tests__/validators.test.ts +197 -3
- package/src/modules/billing/acl.ts +12 -0
- package/src/modules/billing/api/accounts/[id]/summary/route.ts +62 -0
- package/src/modules/billing/api/accounts/route.ts +59 -4
- package/src/modules/billing/api/export/account/[id]/route.ts +13 -11
- package/src/modules/billing/api/invoices/[id]/memos/route.ts +63 -0
- package/src/modules/billing/api/invoices/[id]/pdf/route.ts +80 -0
- package/src/modules/billing/api/invoices/[id]/route.ts +57 -70
- package/src/modules/billing/api/invoices/[id]/send/route.ts +127 -0
- package/src/modules/billing/api/invoices/add-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/delete-draft/route.ts +33 -0
- package/src/modules/billing/api/invoices/edit-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/post/route.ts +7 -6
- package/src/modules/billing/api/invoices/remove-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/route.ts +81 -86
- package/src/modules/billing/api/items/bulk/route.ts +2 -1
- package/src/modules/billing/api/items/change-price/route.ts +54 -0
- package/src/modules/billing/api/items/route.ts +23 -7
- package/src/modules/billing/api/payment-imports/[id]/route.ts +49 -0
- package/src/modules/billing/api/payment-imports/route.ts +57 -0
- package/src/modules/billing/api/payments/[id]/route.ts +63 -0
- package/src/modules/billing/api/payments/allocate/route.ts +35 -0
- package/src/modules/billing/api/payments/assign/route.ts +36 -0
- package/src/modules/billing/api/payments/convert-to-topup/route.ts +35 -0
- package/src/modules/billing/api/payments/ignore/route.ts +35 -0
- package/src/modules/billing/api/payments/import/route.ts +72 -0
- package/src/modules/billing/api/payments/restore/route.ts +35 -0
- package/src/modules/billing/api/payments/route.ts +147 -0
- package/src/modules/billing/api/payments/unallocate/route.ts +35 -0
- package/src/modules/billing/api/payments/unassign/route.ts +35 -0
- package/src/modules/billing/api/run-outcomes/route.ts +49 -6
- package/src/modules/billing/api/runs/[id]/post-drafts/route.ts +71 -0
- package/src/modules/billing/api/runs/[id]/route.ts +59 -0
- package/src/modules/billing/api/runs/retry-failed/route.ts +2 -1
- package/src/modules/billing/api/runs/route.ts +15 -7
- package/src/modules/billing/api/settings/number-preview/route.ts +46 -0
- package/src/modules/billing/api/settings/route.ts +116 -0
- package/src/modules/billing/api/statements/route.ts +5 -0
- package/src/modules/billing/api/test-invoices/wipe/route.ts +2 -2
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +151 -372
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +6 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +70 -4
- package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +444 -95
- package/src/modules/billing/backend/billing/invoices/page.tsx +8 -227
- package/src/modules/billing/backend/billing/items/[id]/page.tsx +75 -6
- package/src/modules/billing/backend/billing/items/create/page.tsx +13 -8
- package/src/modules/billing/backend/billing/items/page.tsx +18 -2
- package/src/modules/billing/backend/billing/payments/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/payments/[id]/page.tsx +415 -0
- package/src/modules/billing/backend/billing/payments/imports/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/payments/imports/[id]/page.tsx +106 -0
- package/src/modules/billing/backend/billing/payments/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/payments/page.tsx +56 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.tsx +49 -18
- package/src/modules/billing/backend/billing/runs/page.tsx +59 -8
- package/src/modules/billing/backend/billing/statements/[id]/page.tsx +86 -5
- package/src/modules/billing/backend/billing/statements/page.tsx +1 -1
- package/src/modules/billing/backend/billing/topups/page.tsx +1 -1
- package/src/modules/billing/backend/billing/transactions/page.tsx +1 -1
- package/src/modules/billing/backend/billing/usage/create/page.tsx +3 -2
- package/src/modules/billing/backend/config/billing/page.meta.ts +21 -0
- package/src/modules/billing/backend/config/billing/page.tsx +549 -0
- package/src/modules/billing/cli.ts +31 -1
- package/src/modules/billing/commands/accounts.ts +6 -0
- package/src/modules/billing/commands/index.ts +3 -0
- package/src/modules/billing/commands/invoices.ts +364 -231
- package/src/modules/billing/commands/items.ts +143 -0
- package/src/modules/billing/commands/memos.ts +218 -0
- package/src/modules/billing/commands/payments.ts +428 -0
- package/src/modules/billing/commands/runs.ts +205 -3
- package/src/modules/billing/commands/settings.ts +77 -0
- package/src/modules/billing/commands/topups.ts +12 -3
- package/src/modules/billing/commands/transactions.ts +2 -2
- package/src/modules/billing/components/AccountForm.tsx +199 -53
- package/src/modules/billing/components/AccountInvoicesSection.tsx +75 -0
- package/src/modules/billing/components/AccountPaymentsSection.tsx +42 -0
- package/src/modules/billing/components/AccountPicker.tsx +70 -0
- package/src/modules/billing/components/AllocatePaymentDialog.tsx +167 -0
- package/src/modules/billing/components/BillingDataTable.tsx +34 -0
- package/src/modules/billing/components/BillingSettingsLink.tsx +25 -0
- package/src/modules/billing/components/ChangeItemPriceDialog.tsx +180 -0
- package/src/modules/billing/components/CreateMemoDialog.tsx +232 -0
- package/src/modules/billing/components/CustomerLabel.tsx +53 -0
- package/src/modules/billing/components/InvoicesTable.tsx +344 -0
- package/src/modules/billing/components/ItemForm.tsx +233 -44
- package/src/modules/billing/components/ItemPriceField.tsx +81 -0
- package/src/modules/billing/components/LineFormDialog.tsx +25 -1
- package/src/modules/billing/components/ManualPaymentDialog.tsx +140 -0
- package/src/modules/billing/components/MoneyInput.tsx +26 -0
- package/src/modules/billing/components/PaymentUploadDialog.tsx +148 -0
- package/src/modules/billing/components/PaymentsTable.tsx +213 -0
- package/src/modules/billing/components/PostRunDraftsButton.tsx +129 -0
- package/src/modules/billing/components/PrepaidPanel.tsx +19 -6
- package/src/modules/billing/components/RunCyclesSection.tsx +298 -0
- package/src/modules/billing/components/TestRunActions.tsx +152 -0
- package/src/modules/billing/components/account/AccountItemsTab.tsx +192 -0
- package/src/modules/billing/components/account/AccountRunBillingDialog.tsx +147 -0
- package/src/modules/billing/components/account/AccountRunsTab.tsx +201 -0
- package/src/modules/billing/components/account/AccountStatementsTab.tsx +120 -0
- package/src/modules/billing/components/account/AccountSummaryCards.tsx +153 -0
- package/src/modules/billing/components/account/AccountUsageTab.tsx +122 -0
- package/src/modules/billing/components/account/AccountView.tsx +101 -0
- package/src/modules/billing/components/account/accountRow.ts +70 -0
- package/src/modules/billing/components/customerNames.ts +62 -0
- package/src/modules/billing/data/entities.ts +656 -6
- package/src/modules/billing/data/entityIds.ts +9 -0
- package/src/modules/billing/data/validators.ts +388 -8
- package/src/modules/billing/events.ts +56 -3
- package/src/modules/billing/i18n/de.json +880 -0
- package/src/modules/billing/i18n/en.json +457 -21
- package/src/modules/billing/i18n/es.json +880 -0
- package/src/modules/billing/i18n/pl.json +458 -22
- package/src/modules/billing/index.ts +6 -2
- package/src/modules/billing/lib/accountFinancials.ts +84 -0
- package/src/modules/billing/lib/accountLock.ts +32 -0
- package/src/modules/billing/lib/accountStatus.ts +11 -9
- package/src/modules/billing/lib/accountSummary.ts +172 -0
- package/src/modules/billing/lib/allocation.ts +515 -0
- package/src/modules/billing/lib/allocationEvents.ts +26 -0
- package/src/modules/billing/lib/billPeriod.ts +19 -0
- package/src/modules/billing/lib/billRunEngine.ts +350 -150
- package/src/modules/billing/lib/billingSettings.ts +145 -0
- package/src/modules/billing/lib/commandRoute.ts +112 -0
- package/src/modules/billing/lib/customerLookup.ts +157 -0
- package/src/modules/billing/lib/dateOnly.ts +22 -0
- package/src/modules/billing/lib/documentParties.ts +23 -0
- package/src/modules/billing/lib/documentTypes.ts +37 -0
- package/src/modules/billing/lib/dueDate.ts +26 -0
- package/src/modules/billing/lib/iban.ts +36 -0
- package/src/modules/billing/lib/invoiceDelivery.ts +196 -0
- package/src/modules/billing/lib/invoiceLanguages.ts +57 -0
- package/src/modules/billing/lib/invoiceWriter.ts +162 -148
- package/src/modules/billing/lib/itemBillingState.ts +67 -0
- package/src/modules/billing/lib/itemPriceChange.ts +56 -0
- package/src/modules/billing/lib/itemSelector.ts +4 -1
- package/src/modules/billing/lib/lineAmounts.ts +61 -0
- package/src/modules/billing/lib/memoGuards.ts +41 -0
- package/src/modules/billing/lib/navigation.ts +64 -0
- package/src/modules/billing/lib/numbering.ts +174 -0
- package/src/modules/billing/lib/overdue.ts +60 -0
- package/src/modules/billing/lib/paymentCsv.ts +171 -0
- package/src/modules/billing/lib/paymentGateway.ts +52 -0
- package/src/modules/billing/lib/paymentImport.ts +228 -0
- package/src/modules/billing/lib/paymentMatcher.ts +88 -0
- package/src/modules/billing/lib/pdf/attachmentStore.ts +60 -0
- package/src/modules/billing/lib/pdf/fonts/NotoSans-Bold.ttf +0 -0
- package/src/modules/billing/lib/pdf/fonts/NotoSans-Regular.ttf +0 -0
- package/src/modules/billing/lib/pdf/fonts/OFL.txt +93 -0
- package/src/modules/billing/lib/pdf/fonts/embeddedFonts.ts +9 -0
- package/src/modules/billing/lib/pdf/invoiceModel.ts +329 -0
- package/src/modules/billing/lib/pdf/invoicePdf.ts +220 -0
- package/src/modules/billing/lib/pdf/invoiceTemplate.ts +338 -0
- package/src/modules/billing/lib/prepaidConfig.ts +27 -36
- package/src/modules/billing/lib/prepaidConsume.ts +29 -6
- package/src/modules/billing/lib/prepaidPeriodClose.ts +407 -239
- package/src/modules/billing/lib/prepaidTaxPoint.ts +76 -0
- package/src/modules/billing/lib/seeds/demo.ts +79 -6
- package/src/modules/billing/lib/seeds/paymentsSample.ts +136 -0
- package/src/modules/billing/lib/seeds/sellerSettings.ts +78 -0
- package/src/modules/billing/lib/servicePeriods.ts +226 -0
- package/src/modules/billing/lib/testRunCleanup.ts +42 -0
- package/src/modules/billing/lib/topupCapture.ts +3 -4
- package/src/modules/billing/lib/topupReceipt.ts +70 -103
- package/src/modules/billing/lib/usageRater.ts +1 -1
- package/src/modules/billing/lib/usageRunner.ts +17 -5
- package/src/modules/billing/lib/vat.ts +56 -0
- package/src/modules/billing/migrations/Migration20261114000000_billing_customer_kind.ts +25 -0
- package/src/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.ts +37 -0
- package/src/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.ts +132 -0
- package/src/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.ts +58 -0
- package/src/modules/billing/migrations/Migration20261117000000_billing_payments.ts +97 -0
- package/src/modules/billing/migrations/Migration20261118000000_billing_memos.ts +21 -0
- package/src/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.ts +39 -0
- package/src/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.ts +35 -0
- package/src/modules/billing/setup.ts +11 -237
- package/src/modules/billing/subscribers/credit-memo-auto-send.ts +15 -0
- package/src/modules/billing/subscribers/debit-memo-apply-credit.ts +15 -0
- package/src/modules/billing/subscribers/debit-memo-auto-send.ts +15 -0
- package/src/modules/billing/subscribers/invoice-apply-credit.ts +39 -0
- package/src/modules/billing/subscribers/invoice-auto-send.ts +46 -0
- package/src/modules/billing/subscribers/payment-captured-to-paid.ts +84 -62
- package/src/modules/billing/subscribers/prepaid-topup-captured.ts +4 -8
- package/dist/modules/billing/lib/invoiceStatus.js +0 -38
- package/dist/modules/billing/lib/invoiceStatus.js.map +0 -7
- package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +0 -97
- package/src/modules/billing/lib/invoiceStatus.ts +0 -61
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'
|
|
3
|
+
import { registerCommand } from '@open-mercato/shared/lib/commands'
|
|
4
|
+
import { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
|
|
5
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
6
|
+
import { BillingAccount, BillingPayment, BillingTopup } from '../data/entities'
|
|
7
|
+
import {
|
|
8
|
+
billingAllocationRemoveSchema,
|
|
9
|
+
billingPaymentAllocateSchema,
|
|
10
|
+
billingPaymentAssignSchema,
|
|
11
|
+
billingPaymentConvertSchema,
|
|
12
|
+
billingPaymentCreateSchema,
|
|
13
|
+
billingPaymentIdSchema,
|
|
14
|
+
billingPaymentImportSchema,
|
|
15
|
+
type BillingAllocationRemoveInput,
|
|
16
|
+
type BillingPaymentAllocateInput,
|
|
17
|
+
type BillingPaymentAssignInput,
|
|
18
|
+
type BillingPaymentConvertInput,
|
|
19
|
+
type BillingPaymentCreateInput,
|
|
20
|
+
type BillingPaymentIdInput,
|
|
21
|
+
type BillingPaymentImportInput,
|
|
22
|
+
} from '../data/validators'
|
|
23
|
+
import { emitBillingEvent } from '../events'
|
|
24
|
+
import { withAccountLock } from '../lib/accountLock'
|
|
25
|
+
import {
|
|
26
|
+
allocate,
|
|
27
|
+
applyAccountCredit,
|
|
28
|
+
emptyEffects,
|
|
29
|
+
mergeEffects,
|
|
30
|
+
toCents,
|
|
31
|
+
unallocate,
|
|
32
|
+
type AllocationEffects,
|
|
33
|
+
type ArScope,
|
|
34
|
+
} from '../lib/allocation'
|
|
35
|
+
import { emitAllocationEffects } from '../lib/allocationEvents'
|
|
36
|
+
import { getBillingSettings, receiptTaxRate } from '../lib/billingSettings'
|
|
37
|
+
import { importPayments, type ImportSummary } from '../lib/paymentImport'
|
|
38
|
+
import { captureTopup } from '../lib/topupCapture'
|
|
39
|
+
import { createHash } from 'node:crypto'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Payment commands (Spec C). Every receivables change runs inside the
|
|
43
|
+
* per-account lock (`withAccountLock`); events fire after commit.
|
|
44
|
+
*
|
|
45
|
+
* Reversals are explicit inverse commands, consistent with the rest of the
|
|
46
|
+
* billing module (which does not use the framework undo log): allocate ↔
|
|
47
|
+
* unallocate, assign ↔ unassign, ignore ↔ restore. Convert-to-top-up is
|
|
48
|
+
* terminal (reverse through the prepaid adjustment flow, C-D3).
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
function getEm(ctx: CommandRuntimeContext): EntityManager {
|
|
52
|
+
return (ctx.container.resolve('em') as EntityManager).fork()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function userIdOf(ctx: CommandRuntimeContext): string | null {
|
|
56
|
+
return (ctx.auth?.sub as string | undefined) ?? null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scopeOf(input: { tenantId: string; organizationId: string }): ArScope {
|
|
60
|
+
return { tenantId: input.tenantId, organizationId: input.organizationId }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function loadPayment(em: EntityManager, scope: ArScope, paymentId: string): Promise<BillingPayment> {
|
|
64
|
+
const payment = await em.findOne(BillingPayment, { id: paymentId, ...scope, deletedAt: null } as never)
|
|
65
|
+
if (!payment) throw new CrudHttpError(404, { error: 'Payment not found', code: 'billing.payment.not_found' })
|
|
66
|
+
return payment
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function loadAccount(em: EntityManager, scope: ArScope, billAccountId: string): Promise<BillingAccount> {
|
|
70
|
+
const account = await em.findOne(BillingAccount, { id: billAccountId, ...scope, deletedAt: null } as never)
|
|
71
|
+
if (!account) throw new CrudHttpError(404, { error: 'Billing account not found', code: 'billing.payment.account_missing' })
|
|
72
|
+
return account
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function countActiveAllocations(em: EntityManager, scope: ArScope, paymentId: string): Promise<number> {
|
|
76
|
+
const rows = (await em.execute(
|
|
77
|
+
`SELECT COUNT(*)::int AS n FROM billing_allocations
|
|
78
|
+
WHERE tenant_id = ? AND organization_id = ? AND source_type = 'payment' AND source_id = ? AND deleted_at IS NULL`,
|
|
79
|
+
[scope.tenantId, scope.organizationId, paymentId],
|
|
80
|
+
)) as unknown as Array<{ n: number }>
|
|
81
|
+
return Number(rows?.[0]?.n ?? 0)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type PaymentCommandResult = { paymentId: string; status: string; unallocatedAmount: string }
|
|
85
|
+
|
|
86
|
+
async function paymentResult(em: EntityManager, scope: ArScope, paymentId: string): Promise<PaymentCommandResult> {
|
|
87
|
+
const rows = (await em.execute(
|
|
88
|
+
`SELECT status, unallocated_amount FROM billing_payments WHERE id = ? AND tenant_id = ? AND organization_id = ?`,
|
|
89
|
+
[paymentId, scope.tenantId, scope.organizationId],
|
|
90
|
+
)) as unknown as Array<{ status: string; unallocated_amount: string }>
|
|
91
|
+
return { paymentId, status: rows?.[0]?.status ?? 'unmatched', unallocatedAmount: rows?.[0]?.unallocated_amount ?? '0' }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Import ──────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
const importPaymentsCommand: CommandHandler<BillingPaymentImportInput, ImportSummary> = {
|
|
97
|
+
id: 'billing.payments.import',
|
|
98
|
+
async execute(rawInput, ctx) {
|
|
99
|
+
const parsed = billingPaymentImportSchema.parse(rawInput)
|
|
100
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
101
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
102
|
+
const scope = scopeOf(parsed)
|
|
103
|
+
const { summary, effects, paymentIds } = await importPayments(getEm(ctx), {
|
|
104
|
+
scope,
|
|
105
|
+
fileName: parsed.fileName,
|
|
106
|
+
content: parsed.content,
|
|
107
|
+
userId: userIdOf(ctx),
|
|
108
|
+
})
|
|
109
|
+
for (const paymentId of paymentIds) await emitBillingEvent('billing.payment.created', { ...scope, paymentId, source: 'csv' })
|
|
110
|
+
await emitAllocationEffects(scope, effects)
|
|
111
|
+
await emitBillingEvent('billing.payment_import.completed', { ...scope, importId: summary.importId, counts: summary.counts })
|
|
112
|
+
return summary
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Manual entry ────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const createPaymentCommand: CommandHandler<BillingPaymentCreateInput, PaymentCommandResult> = {
|
|
119
|
+
id: 'billing.payments.create',
|
|
120
|
+
async execute(rawInput, ctx) {
|
|
121
|
+
const parsed = billingPaymentCreateSchema.parse(rawInput)
|
|
122
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
123
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
124
|
+
const scope = scopeOf(parsed)
|
|
125
|
+
const em = getEm(ctx)
|
|
126
|
+
if (parsed.billAccountId) {
|
|
127
|
+
const account = await loadAccount(em, scope, parsed.billAccountId)
|
|
128
|
+
if (account.currencyCode !== parsed.currencyCode) {
|
|
129
|
+
throw new CrudHttpError(409, { error: 'Payment currency differs from the account', code: 'billing.payment.currency_mismatch' })
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const amount = parsed.amount.toFixed(4)
|
|
133
|
+
const now = new Date()
|
|
134
|
+
const payment = em.create(BillingPayment, {
|
|
135
|
+
...scope,
|
|
136
|
+
receivedAt: new Date(`${parsed.receivedAt}T00:00:00Z`),
|
|
137
|
+
amount,
|
|
138
|
+
currencyCode: parsed.currencyCode,
|
|
139
|
+
reference: parsed.reference ?? null,
|
|
140
|
+
payerName: parsed.payerName ?? null,
|
|
141
|
+
payerAccount: parsed.payerAccount ?? null,
|
|
142
|
+
source: 'manual',
|
|
143
|
+
billAccountId: parsed.billAccountId ?? null,
|
|
144
|
+
status: parsed.billAccountId ? 'matched' : 'unmatched',
|
|
145
|
+
unallocatedAmount: amount,
|
|
146
|
+
matchReason: parsed.billAccountId ? null : 'manual',
|
|
147
|
+
sourceRef: `manual:${createHash('sha256').update(`${scope.tenantId}|${now.toISOString()}|${Math.random()}`).digest('hex')}`,
|
|
148
|
+
createdAt: now,
|
|
149
|
+
updatedAt: now,
|
|
150
|
+
})
|
|
151
|
+
em.persist(payment)
|
|
152
|
+
await em.flush()
|
|
153
|
+
let effects = emptyEffects()
|
|
154
|
+
if (parsed.billAccountId) {
|
|
155
|
+
effects = await withAccountLock(em, { tenantId: scope.tenantId, billAccountId: parsed.billAccountId }, (tem) =>
|
|
156
|
+
applyAccountCredit(tem, scope, { id: parsed.billAccountId!, currencyCode: parsed.currencyCode }, {
|
|
157
|
+
onlySources: [{ type: 'payment', id: payment.id }],
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
await emitBillingEvent('billing.payment.created', { ...scope, paymentId: payment.id, source: 'manual' })
|
|
162
|
+
await emitAllocationEffects(scope, effects)
|
|
163
|
+
return paymentResult(em, scope, payment.id)
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Assign / unassign ───────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
const assignPaymentCommand: CommandHandler<BillingPaymentAssignInput, PaymentCommandResult> = {
|
|
170
|
+
id: 'billing.payments.assign',
|
|
171
|
+
async execute(rawInput, ctx) {
|
|
172
|
+
const parsed = billingPaymentAssignSchema.parse(rawInput)
|
|
173
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
174
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
175
|
+
const scope = scopeOf(parsed)
|
|
176
|
+
const em = getEm(ctx)
|
|
177
|
+
const [payment, account] = await Promise.all([
|
|
178
|
+
loadPayment(em, scope, parsed.paymentId),
|
|
179
|
+
loadAccount(em, scope, parsed.billAccountId),
|
|
180
|
+
])
|
|
181
|
+
if (payment.status !== 'unmatched') {
|
|
182
|
+
throw new CrudHttpError(409, { error: 'Only unmatched payments can be assigned', code: 'billing.payment.not_unmatched' })
|
|
183
|
+
}
|
|
184
|
+
if (account.billingMode === 'prepaid') {
|
|
185
|
+
throw new CrudHttpError(409, {
|
|
186
|
+
error: 'Prepaid accounts take payments as top-ups — use "Convert to top-up"',
|
|
187
|
+
code: 'billing.payment.prepaid_account',
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
if (account.currencyCode !== payment.currencyCode) {
|
|
191
|
+
throw new CrudHttpError(409, { error: 'Payment currency differs from the account', code: 'billing.payment.currency_mismatch' })
|
|
192
|
+
}
|
|
193
|
+
const effects = await withAccountLock(em, { tenantId: scope.tenantId, billAccountId: account.id }, async (tem) => {
|
|
194
|
+
await tem.execute(
|
|
195
|
+
`UPDATE billing_payments SET bill_account_id = ?, status = 'matched', match_reason = 'manual', updated_at = now()
|
|
196
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ? AND status = 'unmatched'`,
|
|
197
|
+
[account.id, payment.id, scope.tenantId, scope.organizationId],
|
|
198
|
+
)
|
|
199
|
+
return applyAccountCredit(tem, scope, { id: account.id, currencyCode: account.currencyCode }, {
|
|
200
|
+
onlySources: [{ type: 'payment', id: payment.id }],
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
await emitAllocationEffects(scope, effects)
|
|
204
|
+
return paymentResult(em, scope, payment.id)
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const unassignPaymentCommand: CommandHandler<BillingPaymentIdInput, PaymentCommandResult> = {
|
|
209
|
+
id: 'billing.payments.unassign',
|
|
210
|
+
async execute(rawInput, ctx) {
|
|
211
|
+
const parsed = billingPaymentIdSchema.parse(rawInput)
|
|
212
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
213
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
214
|
+
const scope = scopeOf(parsed)
|
|
215
|
+
const em = getEm(ctx)
|
|
216
|
+
const payment = await loadPayment(em, scope, parsed.paymentId)
|
|
217
|
+
if (payment.status !== 'matched' || !payment.billAccountId) {
|
|
218
|
+
throw new CrudHttpError(409, { error: 'Only fully unallocated matched payments can be unassigned', code: 'billing.payment.not_matched' })
|
|
219
|
+
}
|
|
220
|
+
if ((await countActiveAllocations(em, scope, payment.id)) > 0) {
|
|
221
|
+
throw new CrudHttpError(409, { error: 'Un-allocate the payment first', code: 'billing.payment.has_allocations' })
|
|
222
|
+
}
|
|
223
|
+
await em.execute(
|
|
224
|
+
`UPDATE billing_payments SET bill_account_id = NULL, status = 'unmatched', match_reason = 'manual', updated_at = now()
|
|
225
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ?`,
|
|
226
|
+
[payment.id, scope.tenantId, scope.organizationId],
|
|
227
|
+
)
|
|
228
|
+
return paymentResult(em, scope, payment.id)
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Allocate / unallocate ───────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
const allocatePaymentCommand: CommandHandler<BillingPaymentAllocateInput, PaymentCommandResult> = {
|
|
235
|
+
id: 'billing.payments.allocate',
|
|
236
|
+
async execute(rawInput, ctx) {
|
|
237
|
+
const parsed = billingPaymentAllocateSchema.parse(rawInput)
|
|
238
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
239
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
240
|
+
const scope = scopeOf(parsed)
|
|
241
|
+
const em = getEm(ctx)
|
|
242
|
+
const payment = await loadPayment(em, scope, parsed.paymentId)
|
|
243
|
+
if (!payment.billAccountId) {
|
|
244
|
+
throw new CrudHttpError(409, { error: 'Assign the payment to an account first', code: 'billing.payment.unassigned' })
|
|
245
|
+
}
|
|
246
|
+
const effects = await withAccountLock(em, { tenantId: scope.tenantId, billAccountId: payment.billAccountId }, async (tem) => {
|
|
247
|
+
const all = emptyEffects()
|
|
248
|
+
for (const entry of parsed.allocations) {
|
|
249
|
+
mergeEffects(
|
|
250
|
+
all,
|
|
251
|
+
await allocate(tem, scope, {
|
|
252
|
+
source: { type: 'payment', id: payment.id },
|
|
253
|
+
invoiceId: entry.invoiceId,
|
|
254
|
+
amountCents: toCents(entry.amount),
|
|
255
|
+
by: 'user',
|
|
256
|
+
userId: userIdOf(ctx),
|
|
257
|
+
}),
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
return all
|
|
261
|
+
})
|
|
262
|
+
await emitAllocationEffects(scope, effects)
|
|
263
|
+
return paymentResult(em, scope, payment.id)
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Source-agnostic: removes a payment OR credit memo allocation (Spec D P-D10). */
|
|
268
|
+
const unallocateCommand: CommandHandler<BillingAllocationRemoveInput, { allocationId: string; effects: AllocationEffects }> = {
|
|
269
|
+
id: 'billing.payments.unallocate',
|
|
270
|
+
async execute(rawInput, ctx) {
|
|
271
|
+
const parsed = billingAllocationRemoveSchema.parse(rawInput)
|
|
272
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
273
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
274
|
+
const scope = scopeOf(parsed)
|
|
275
|
+
const em = getEm(ctx)
|
|
276
|
+
const rows = (await em.execute(
|
|
277
|
+
`SELECT bill_account_id FROM billing_allocations
|
|
278
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL`,
|
|
279
|
+
[parsed.allocationId, scope.tenantId, scope.organizationId],
|
|
280
|
+
)) as unknown as Array<{ bill_account_id: string }>
|
|
281
|
+
const billAccountId = rows?.[0]?.bill_account_id
|
|
282
|
+
if (!billAccountId) throw new CrudHttpError(404, { error: 'Allocation not found', code: 'billing.allocation.not_found' })
|
|
283
|
+
const effects = await withAccountLock(em, { tenantId: scope.tenantId, billAccountId }, (tem) =>
|
|
284
|
+
unallocate(tem, scope, { allocationId: parsed.allocationId, userId: userIdOf(ctx) }),
|
|
285
|
+
)
|
|
286
|
+
await emitAllocationEffects(scope, effects)
|
|
287
|
+
return { allocationId: parsed.allocationId, effects }
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ─── Ignore / restore ────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
const ignorePaymentCommand: CommandHandler<BillingPaymentIdInput, PaymentCommandResult> = {
|
|
294
|
+
id: 'billing.payments.ignore',
|
|
295
|
+
async execute(rawInput, ctx) {
|
|
296
|
+
const parsed = billingPaymentIdSchema.parse(rawInput)
|
|
297
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
298
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
299
|
+
const scope = scopeOf(parsed)
|
|
300
|
+
const em = getEm(ctx)
|
|
301
|
+
const payment = await loadPayment(em, scope, parsed.paymentId)
|
|
302
|
+
if (!['unmatched', 'matched'].includes(payment.status) || (await countActiveAllocations(em, scope, payment.id)) > 0) {
|
|
303
|
+
throw new CrudHttpError(409, { error: 'Only unallocated payments can be ignored', code: 'billing.payment.has_allocations' })
|
|
304
|
+
}
|
|
305
|
+
await em.execute(
|
|
306
|
+
`UPDATE billing_payments SET status = 'ignored', metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object('ignored_from', ?::text), updated_at = now()
|
|
307
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ?`,
|
|
308
|
+
[payment.status, payment.id, scope.tenantId, scope.organizationId],
|
|
309
|
+
)
|
|
310
|
+
return paymentResult(em, scope, payment.id)
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const restorePaymentCommand: CommandHandler<BillingPaymentIdInput, PaymentCommandResult> = {
|
|
315
|
+
id: 'billing.payments.restore',
|
|
316
|
+
async execute(rawInput, ctx) {
|
|
317
|
+
const parsed = billingPaymentIdSchema.parse(rawInput)
|
|
318
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
319
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
320
|
+
const scope = scopeOf(parsed)
|
|
321
|
+
const em = getEm(ctx)
|
|
322
|
+
const payment = await loadPayment(em, scope, parsed.paymentId)
|
|
323
|
+
if (payment.status !== 'ignored') {
|
|
324
|
+
throw new CrudHttpError(409, { error: 'Only ignored payments can be restored', code: 'billing.payment.not_ignored' })
|
|
325
|
+
}
|
|
326
|
+
const status = payment.billAccountId ? 'matched' : 'unmatched'
|
|
327
|
+
let effects = emptyEffects()
|
|
328
|
+
if (payment.billAccountId) {
|
|
329
|
+
const account = await loadAccount(em, scope, payment.billAccountId)
|
|
330
|
+
effects = await withAccountLock(em, { tenantId: scope.tenantId, billAccountId: account.id }, async (tem) => {
|
|
331
|
+
await tem.execute(`UPDATE billing_payments SET status = ?, updated_at = now() WHERE id = ?`, [status, payment.id])
|
|
332
|
+
return applyAccountCredit(tem, scope, { id: account.id, currencyCode: account.currencyCode }, {
|
|
333
|
+
onlySources: [{ type: 'payment', id: payment.id }],
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
} else {
|
|
337
|
+
await em.execute(`UPDATE billing_payments SET status = ?, updated_at = now() WHERE id = ?`, [status, payment.id])
|
|
338
|
+
}
|
|
339
|
+
await emitAllocationEffects(scope, effects)
|
|
340
|
+
return paymentResult(em, scope, payment.id)
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── Convert to top-up (C-D3) ────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
const convertToTopupCommand: CommandHandler<BillingPaymentConvertInput, PaymentCommandResult & { topupId: string }> = {
|
|
347
|
+
id: 'billing.payments.convert_to_topup',
|
|
348
|
+
async execute(rawInput, ctx) {
|
|
349
|
+
const parsed = billingPaymentConvertSchema.parse(rawInput)
|
|
350
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
351
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
352
|
+
const scope = scopeOf(parsed)
|
|
353
|
+
const em = getEm(ctx)
|
|
354
|
+
const [payment, account] = await Promise.all([
|
|
355
|
+
loadPayment(em, scope, parsed.paymentId),
|
|
356
|
+
loadAccount(em, scope, parsed.billAccountId),
|
|
357
|
+
])
|
|
358
|
+
if (account.billingMode !== 'prepaid') {
|
|
359
|
+
throw new CrudHttpError(409, { error: 'Only prepaid accounts take top-ups', code: 'billing.payment.not_prepaid' })
|
|
360
|
+
}
|
|
361
|
+
if (account.currencyCode !== payment.currencyCode) {
|
|
362
|
+
throw new CrudHttpError(409, { error: 'Payment currency differs from the account', code: 'billing.payment.currency_mismatch' })
|
|
363
|
+
}
|
|
364
|
+
const sourceRef = `topup-${payment.id}`
|
|
365
|
+
if (payment.status === 'converted_to_topup' && payment.topupId) {
|
|
366
|
+
return { ...(await paymentResult(em, scope, payment.id)), topupId: payment.topupId }
|
|
367
|
+
}
|
|
368
|
+
if (payment.status !== 'unmatched' || toCents(payment.unallocatedAmount) !== toCents(payment.amount)) {
|
|
369
|
+
throw new CrudHttpError(409, { error: 'Only unmatched, unallocated payments can be converted', code: 'billing.payment.not_convertible' })
|
|
370
|
+
}
|
|
371
|
+
let topup = await em.findOne(BillingTopup, { ...scope, sourceRef } as never)
|
|
372
|
+
if (!topup) {
|
|
373
|
+
const now = new Date()
|
|
374
|
+
topup = em.create(BillingTopup, {
|
|
375
|
+
...scope,
|
|
376
|
+
billAccountId: account.id,
|
|
377
|
+
status: 'pending',
|
|
378
|
+
amount: payment.amount,
|
|
379
|
+
currencyCode: payment.currencyCode,
|
|
380
|
+
providerKey: 'bank_transfer',
|
|
381
|
+
paymentId: payment.id,
|
|
382
|
+
sourceRef,
|
|
383
|
+
metadata: { billing_payment_id: payment.id, reference: payment.reference ?? null },
|
|
384
|
+
createdAt: now,
|
|
385
|
+
updatedAt: now,
|
|
386
|
+
} as never)
|
|
387
|
+
em.persist(topup)
|
|
388
|
+
await em.flush()
|
|
389
|
+
}
|
|
390
|
+
const settings = await getBillingSettings(em, scope)
|
|
391
|
+
await captureTopup(em, {
|
|
392
|
+
topup,
|
|
393
|
+
account: { id: account.id, organizationId: account.organizationId, tenantId: account.tenantId, currencyCode: account.currencyCode },
|
|
394
|
+
taxRate: receiptTaxRate(settings),
|
|
395
|
+
gatewayTransactionId: payment.reference ?? null,
|
|
396
|
+
})
|
|
397
|
+
await em.execute(
|
|
398
|
+
`UPDATE billing_payments
|
|
399
|
+
SET status = 'converted_to_topup', bill_account_id = ?, topup_id = ?, unallocated_amount = 0, updated_at = now()
|
|
400
|
+
WHERE id = ? AND tenant_id = ? AND organization_id = ?`,
|
|
401
|
+
[account.id, topup.id, payment.id, scope.tenantId, scope.organizationId],
|
|
402
|
+
)
|
|
403
|
+
await emitBillingEvent('billing.payment.converted_to_topup', { ...scope, paymentId: payment.id, topupId: topup.id, billAccountId: account.id })
|
|
404
|
+
return { ...(await paymentResult(em, scope, payment.id)), topupId: topup.id }
|
|
405
|
+
},
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
registerCommand(importPaymentsCommand)
|
|
409
|
+
registerCommand(createPaymentCommand)
|
|
410
|
+
registerCommand(assignPaymentCommand)
|
|
411
|
+
registerCommand(unassignPaymentCommand)
|
|
412
|
+
registerCommand(allocatePaymentCommand)
|
|
413
|
+
registerCommand(unallocateCommand)
|
|
414
|
+
registerCommand(ignorePaymentCommand)
|
|
415
|
+
registerCommand(restorePaymentCommand)
|
|
416
|
+
registerCommand(convertToTopupCommand)
|
|
417
|
+
|
|
418
|
+
export {
|
|
419
|
+
importPaymentsCommand,
|
|
420
|
+
createPaymentCommand,
|
|
421
|
+
assignPaymentCommand,
|
|
422
|
+
unassignPaymentCommand,
|
|
423
|
+
allocatePaymentCommand,
|
|
424
|
+
unallocateCommand,
|
|
425
|
+
ignorePaymentCommand,
|
|
426
|
+
restorePaymentCommand,
|
|
427
|
+
convertToTopupCommand,
|
|
428
|
+
}
|
|
@@ -8,13 +8,21 @@ import {
|
|
|
8
8
|
} from '@open-mercato/shared/lib/commands/scope'
|
|
9
9
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
10
|
import { BillingRun, BillingRunOutcome } from '../data/entities'
|
|
11
|
+
import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
12
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
11
13
|
import {
|
|
14
|
+
billingRunDeleteSchema,
|
|
15
|
+
billingRunPostDraftsSchema,
|
|
16
|
+
type BillingRunPostDraftsInput,
|
|
12
17
|
billingRunRetryFailedSchema,
|
|
13
18
|
billingRunTriggerSchema,
|
|
19
|
+
type BillingRunDeleteInput,
|
|
14
20
|
type BillingRunRetryFailedInput,
|
|
15
21
|
type BillingRunTriggerInput,
|
|
16
22
|
} from '../data/validators'
|
|
23
|
+
import { emitBillingEvent } from '../events'
|
|
17
24
|
import { runBillRun } from '../lib/billRunEngine'
|
|
25
|
+
import { deleteTestRunArtifacts } from '../lib/testRunCleanup'
|
|
18
26
|
import { TenantLockBusyError } from '../lib/tenantLock'
|
|
19
27
|
|
|
20
28
|
/**
|
|
@@ -71,7 +79,6 @@ const triggerRunCommand: CommandHandler<BillingRunTriggerInput, TriggerResult> =
|
|
|
71
79
|
triggeredBy: 'manual',
|
|
72
80
|
mode: parsed.mode,
|
|
73
81
|
scopedAccountIds: parsed.scopedAccountIds,
|
|
74
|
-
catchUp: parsed.catchUp,
|
|
75
82
|
})
|
|
76
83
|
return {
|
|
77
84
|
billRunId: billRun.id,
|
|
@@ -139,7 +146,6 @@ const retryFailedCommand: CommandHandler<BillingRunRetryFailedInput, TriggerResu
|
|
|
139
146
|
mode: parsed.mode,
|
|
140
147
|
scopedAccountIds,
|
|
141
148
|
parentRunId: parentRun.id,
|
|
142
|
-
catchUp: parentRun.catchUp,
|
|
143
149
|
})
|
|
144
150
|
return {
|
|
145
151
|
billRunId: billRun.id,
|
|
@@ -160,7 +166,203 @@ const retryFailedCommand: CommandHandler<BillingRunRetryFailedInput, TriggerResu
|
|
|
160
166
|
},
|
|
161
167
|
}
|
|
162
168
|
|
|
169
|
+
// ─── Delete (Spec F, F-D1) ───────────────────────────────────────
|
|
170
|
+
//
|
|
171
|
+
// Test and dry runs are scratch data: deleting one removes its test
|
|
172
|
+
// invoices (with lines and draft-edit audit rows), its outcomes and the run
|
|
173
|
+
// row, in one transaction. Real runs are the audit trail and are refused.
|
|
174
|
+
// Retry children keep existing; their parent link is cleared. Not undoable,
|
|
175
|
+
// like the test-invoice wipe.
|
|
176
|
+
|
|
177
|
+
type DeleteRunResult = { ok: true; invoicesRemoved: number }
|
|
178
|
+
|
|
179
|
+
export function runMode(run: Pick<BillingRun, 'dryRun' | 'testMode'>): 'real' | 'test' | 'dry' {
|
|
180
|
+
if (run.dryRun) return 'dry'
|
|
181
|
+
if (run.testMode) return 'test'
|
|
182
|
+
return 'real'
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const deleteRunCommand: CommandHandler<BillingRunDeleteInput, DeleteRunResult> = {
|
|
186
|
+
id: 'billing.runs.delete',
|
|
187
|
+
|
|
188
|
+
async execute(rawInput, ctx) {
|
|
189
|
+
const parsed = billingRunDeleteSchema.parse(rawInput)
|
|
190
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
191
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
192
|
+
|
|
193
|
+
const em = getEm(ctx)
|
|
194
|
+
const outcome = await em.transactional(async (tem) => {
|
|
195
|
+
const run = await tem.findOne(BillingRun, {
|
|
196
|
+
id: parsed.billRunId,
|
|
197
|
+
tenantId: parsed.tenantId,
|
|
198
|
+
organizationId: parsed.organizationId,
|
|
199
|
+
deletedAt: null,
|
|
200
|
+
})
|
|
201
|
+
// 404 for another organization's run too (no existence leak).
|
|
202
|
+
if (!run) throw new CrudHttpError(404, { error: 'Bill Run not found', code: 'billing.run.not_found' })
|
|
203
|
+
const mode = runMode(run)
|
|
204
|
+
if (mode === 'real') {
|
|
205
|
+
throw new CrudHttpError(422, {
|
|
206
|
+
error: 'Real Bill Runs cannot be deleted',
|
|
207
|
+
code: 'billing.run.delete_real_forbidden',
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
if (run.status === 'running') {
|
|
211
|
+
throw new CrudHttpError(409, { error: 'The Bill Run is still running', code: 'billing.run.running' })
|
|
212
|
+
}
|
|
213
|
+
const scope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
|
|
214
|
+
const invoicesRemoved = await deleteTestRunArtifacts(tem as EntityManager, { ...scope, billRunId: run.id })
|
|
215
|
+
await tem.execute(
|
|
216
|
+
'UPDATE billing_runs SET parent_run_id = NULL WHERE tenant_id = ? AND organization_id = ? AND parent_run_id = ?',
|
|
217
|
+
[scope.tenantId, scope.organizationId, run.id],
|
|
218
|
+
)
|
|
219
|
+
await tem.execute(
|
|
220
|
+
'DELETE FROM billing_run_outcomes WHERE tenant_id = ? AND organization_id = ? AND bill_run_id = ?',
|
|
221
|
+
[scope.tenantId, scope.organizationId, run.id],
|
|
222
|
+
)
|
|
223
|
+
await tem.execute('DELETE FROM billing_runs WHERE tenant_id = ? AND organization_id = ? AND id = ?', [
|
|
224
|
+
scope.tenantId,
|
|
225
|
+
scope.organizationId,
|
|
226
|
+
run.id,
|
|
227
|
+
])
|
|
228
|
+
return { mode, invoicesRemoved }
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// After commit: subscribers never see a phantom delete.
|
|
232
|
+
await emitBillingEvent(
|
|
233
|
+
'billing.run.deleted',
|
|
234
|
+
{
|
|
235
|
+
billRunId: parsed.billRunId,
|
|
236
|
+
tenantId: parsed.tenantId,
|
|
237
|
+
organizationId: parsed.organizationId,
|
|
238
|
+
mode: outcome.mode,
|
|
239
|
+
invoicesRemoved: outcome.invoicesRemoved,
|
|
240
|
+
},
|
|
241
|
+
{ persistent: true },
|
|
242
|
+
)
|
|
243
|
+
return { ok: true, invoicesRemoved: outcome.invoicesRemoved }
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── Post the run's drafts ──────────────────────────────────────
|
|
248
|
+
//
|
|
249
|
+
// Posts the draft invoices a REAL run wrote, in bounded batches (ascending
|
|
250
|
+
// id, keyset cursor) so a run with thousands of invoices never becomes one
|
|
251
|
+
// long request. Each invoice goes through `billing.invoices.post` on its own
|
|
252
|
+
// — numbering, frozen PDF, due date, credit auto-apply, auto-send and events
|
|
253
|
+
// stay exactly as for a single post — so one failure never undoes the
|
|
254
|
+
// others. A seller-settings failure would fail every invoice, so the batch
|
|
255
|
+
// stops at the first one and says so.
|
|
256
|
+
|
|
257
|
+
export const POST_DRAFTS_DEFAULT_LIMIT = 25
|
|
258
|
+
|
|
259
|
+
type PostDraftsFailure = { invoiceId: string; number: string; error: string; code: string | null }
|
|
260
|
+
|
|
261
|
+
export type PostDraftsResult = {
|
|
262
|
+
posted: number
|
|
263
|
+
failed: PostDraftsFailure[]
|
|
264
|
+
/** Last invoice id handled — pass back as `afterInvoiceId`. Null when nothing was processed. */
|
|
265
|
+
nextCursor: string | null
|
|
266
|
+
/** Drafts of the run after the cursor that are still to process. */
|
|
267
|
+
remaining: number
|
|
268
|
+
/** Set when the batch stopped early (e.g. `billing.settings.incomplete`). */
|
|
269
|
+
stoppedReason: string | null
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const DRAFTS_AFTER_SQL = `SELECT id, number FROM billing_invoices
|
|
273
|
+
WHERE tenant_id = ? AND organization_id = ? AND bill_run_id = ?
|
|
274
|
+
AND status = 'draft' AND test_run = false AND document_type = 'invoice' AND deleted_at IS NULL
|
|
275
|
+
AND (?::uuid IS NULL OR id > ?::uuid)
|
|
276
|
+
ORDER BY id
|
|
277
|
+
LIMIT ?`
|
|
278
|
+
|
|
279
|
+
const DRAFTS_REMAINING_SQL = `SELECT COUNT(*)::int AS remaining FROM billing_invoices
|
|
280
|
+
WHERE tenant_id = ? AND organization_id = ? AND bill_run_id = ?
|
|
281
|
+
AND status = 'draft' AND test_run = false AND document_type = 'invoice' AND deleted_at IS NULL
|
|
282
|
+
AND (?::uuid IS NULL OR id > ?::uuid)`
|
|
283
|
+
|
|
284
|
+
const postDraftsCommand: CommandHandler<BillingRunPostDraftsInput, PostDraftsResult> = {
|
|
285
|
+
id: 'billing.runs.post_drafts',
|
|
286
|
+
|
|
287
|
+
async execute(rawInput, ctx) {
|
|
288
|
+
const parsed = billingRunPostDraftsSchema.parse(rawInput)
|
|
289
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
290
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
291
|
+
|
|
292
|
+
const em = getEm(ctx)
|
|
293
|
+
const run = await em.findOne(BillingRun, {
|
|
294
|
+
id: parsed.billRunId,
|
|
295
|
+
tenantId: parsed.tenantId,
|
|
296
|
+
organizationId: parsed.organizationId,
|
|
297
|
+
deletedAt: null,
|
|
298
|
+
})
|
|
299
|
+
if (!run) throw new CrudHttpError(404, { error: 'Bill Run not found', code: 'billing.run.not_found' })
|
|
300
|
+
if (runMode(run) !== 'real') {
|
|
301
|
+
throw new CrudHttpError(422, { error: 'Only real Bill Runs have invoices to post', code: 'billing.run.not_real' })
|
|
302
|
+
}
|
|
303
|
+
if (run.status === 'running') {
|
|
304
|
+
throw new CrudHttpError(409, { error: 'The Bill Run is still running', code: 'billing.run.running' })
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const scope = [parsed.tenantId, parsed.organizationId, run.id] as const
|
|
308
|
+
const cursor = parsed.afterInvoiceId ?? null
|
|
309
|
+
const limit = parsed.limit ?? POST_DRAFTS_DEFAULT_LIMIT
|
|
310
|
+
const drafts = (await em.execute(DRAFTS_AFTER_SQL, [...scope, cursor, cursor, limit])) as unknown as Array<{
|
|
311
|
+
id: string
|
|
312
|
+
number: string
|
|
313
|
+
}>
|
|
314
|
+
|
|
315
|
+
const commandBus = ctx.container.resolve('commandBus') as CommandBus
|
|
316
|
+
const failed: PostDraftsFailure[] = []
|
|
317
|
+
let posted = 0
|
|
318
|
+
let nextCursor: string | null = null
|
|
319
|
+
let stoppedReason: string | null = null
|
|
320
|
+
for (const draft of drafts ?? []) {
|
|
321
|
+
nextCursor = draft.id
|
|
322
|
+
try {
|
|
323
|
+
await commandBus.execute('billing.invoices.post', {
|
|
324
|
+
input: { tenantId: parsed.tenantId, organizationId: parsed.organizationId, invoiceId: draft.id },
|
|
325
|
+
ctx,
|
|
326
|
+
})
|
|
327
|
+
posted += 1
|
|
328
|
+
} catch (err) {
|
|
329
|
+
const body = isCrudHttpError(err) ? (err.body as { error?: string; code?: string }) : null
|
|
330
|
+
const code = body?.code ?? null
|
|
331
|
+
failed.push({
|
|
332
|
+
invoiceId: draft.id,
|
|
333
|
+
number: draft.number,
|
|
334
|
+
error: body?.error ?? (err instanceof Error ? err.message : String(err)),
|
|
335
|
+
code,
|
|
336
|
+
})
|
|
337
|
+
if (code === 'billing.settings.incomplete') {
|
|
338
|
+
// Every other draft would fail the same way — stop. The returned
|
|
339
|
+
// cursor stays where this call started; drafts posted in this call
|
|
340
|
+
// are no longer drafts, so a retry after fixing the settings picks
|
|
341
|
+
// up exactly the ones still pending.
|
|
342
|
+
stoppedReason = code
|
|
343
|
+
break
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const remainingRows = (await em.execute(DRAFTS_REMAINING_SQL, [
|
|
349
|
+
...scope,
|
|
350
|
+
stoppedReason ? cursor : nextCursor,
|
|
351
|
+
stoppedReason ? cursor : nextCursor,
|
|
352
|
+
])) as unknown as Array<{ remaining: number }>
|
|
353
|
+
return {
|
|
354
|
+
posted,
|
|
355
|
+
failed,
|
|
356
|
+
nextCursor: stoppedReason ? cursor : nextCursor,
|
|
357
|
+
remaining: Number(remainingRows?.[0]?.remaining ?? 0),
|
|
358
|
+
stoppedReason,
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
|
|
163
363
|
registerCommand(triggerRunCommand)
|
|
164
364
|
registerCommand(retryFailedCommand)
|
|
365
|
+
registerCommand(deleteRunCommand)
|
|
366
|
+
registerCommand(postDraftsCommand)
|
|
165
367
|
|
|
166
|
-
export { triggerRunCommand, retryFailedCommand }
|
|
368
|
+
export { triggerRunCommand, retryFailedCommand, deleteRunCommand, postDraftsCommand }
|