@dainamite/billing 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/dist/modules/billing/acl.js +13 -1
- package/dist/modules/billing/acl.js.map +2 -2
- package/dist/modules/billing/api/accounts/[id]/summary/route.js +52 -0
- package/dist/modules/billing/api/accounts/[id]/summary/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +46 -4
- package/dist/modules/billing/api/accounts/route.js.map +2 -2
- package/dist/modules/billing/api/export/account/[id]/route.js +11 -7
- package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/[id]/memos/route.js +54 -0
- package/dist/modules/billing/api/invoices/[id]/memos/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/pdf/route.js +69 -0
- package/dist/modules/billing/api/invoices/[id]/pdf/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/route.js +44 -24
- package/dist/modules/billing/api/invoices/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/[id]/send/route.js +107 -0
- package/dist/modules/billing/api/invoices/[id]/send/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/add-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/add-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/delete-draft/route.js +33 -0
- package/dist/modules/billing/api/invoices/delete-draft/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/edit-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/post/route.js +2 -2
- package/dist/modules/billing/api/invoices/post/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/remove-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/remove-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/route.js +45 -46
- package/dist/modules/billing/api/invoices/route.js.map +2 -2
- package/dist/modules/billing/api/items/bulk/route.js +1 -1
- package/dist/modules/billing/api/items/bulk/route.js.map +2 -2
- package/dist/modules/billing/api/items/change-price/route.js +44 -0
- package/dist/modules/billing/api/items/change-price/route.js.map +7 -0
- package/dist/modules/billing/api/items/route.js +19 -7
- package/dist/modules/billing/api/items/route.js.map +2 -2
- package/dist/modules/billing/api/payment-imports/[id]/route.js +49 -0
- package/dist/modules/billing/api/payment-imports/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/payment-imports/route.js +57 -0
- package/dist/modules/billing/api/payment-imports/route.js.map +7 -0
- package/dist/modules/billing/api/payments/[id]/route.js +60 -0
- package/dist/modules/billing/api/payments/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/payments/allocate/route.js +35 -0
- package/dist/modules/billing/api/payments/allocate/route.js.map +7 -0
- package/dist/modules/billing/api/payments/assign/route.js +36 -0
- package/dist/modules/billing/api/payments/assign/route.js.map +7 -0
- package/dist/modules/billing/api/payments/convert-to-topup/route.js +35 -0
- package/dist/modules/billing/api/payments/convert-to-topup/route.js.map +7 -0
- package/dist/modules/billing/api/payments/ignore/route.js +35 -0
- package/dist/modules/billing/api/payments/ignore/route.js.map +7 -0
- package/dist/modules/billing/api/payments/import/route.js +63 -0
- package/dist/modules/billing/api/payments/import/route.js.map +7 -0
- package/dist/modules/billing/api/payments/restore/route.js +35 -0
- package/dist/modules/billing/api/payments/restore/route.js.map +7 -0
- package/dist/modules/billing/api/payments/route.js +114 -0
- package/dist/modules/billing/api/payments/route.js.map +7 -0
- package/dist/modules/billing/api/payments/unallocate/route.js +35 -0
- package/dist/modules/billing/api/payments/unallocate/route.js.map +7 -0
- package/dist/modules/billing/api/payments/unassign/route.js +35 -0
- package/dist/modules/billing/api/payments/unassign/route.js.map +7 -0
- package/dist/modules/billing/api/run-outcomes/route.js +43 -6
- package/dist/modules/billing/api/run-outcomes/route.js.map +2 -2
- package/dist/modules/billing/api/runs/[id]/post-drafts/route.js +61 -0
- package/dist/modules/billing/api/runs/[id]/post-drafts/route.js.map +7 -0
- package/dist/modules/billing/api/runs/[id]/route.js +53 -0
- package/dist/modules/billing/api/runs/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js +1 -1
- package/dist/modules/billing/api/runs/retry-failed/route.js.map +2 -2
- package/dist/modules/billing/api/runs/route.js +9 -3
- package/dist/modules/billing/api/runs/route.js.map +2 -2
- package/dist/modules/billing/api/settings/number-preview/route.js +42 -0
- package/dist/modules/billing/api/settings/number-preview/route.js.map +7 -0
- package/dist/modules/billing/api/settings/route.js +92 -0
- package/dist/modules/billing/api/settings/route.js.map +7 -0
- package/dist/modules/billing/api/statements/route.js +3 -0
- package/dist/modules/billing/api/statements/route.js.map +2 -2
- package/dist/modules/billing/api/test-invoices/wipe/route.js +2 -2
- package/dist/modules/billing/api/test-invoices/wipe/route.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +121 -232
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/create/page.js +6 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/page.js +63 -4
- package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js +297 -65
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +3 -3
- package/dist/modules/billing/backend/billing/invoices/page.js +6 -172
- package/dist/modules/billing/backend/billing/invoices/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/[id]/page.js +51 -7
- package/dist/modules/billing/backend/billing/items/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/create/page.js +9 -6
- package/dist/modules/billing/backend/billing/items/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/page.js +8 -2
- package/dist/modules/billing/backend/billing/items/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/payments/[id]/page.js +297 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js +74 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/page.js +58 -0
- package/dist/modules/billing/backend/billing/payments/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/payments/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js +49 -14
- package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/runs/page.js +58 -7
- package/dist/modules/billing/backend/billing/runs/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/[id]/page.js +61 -6
- package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/page.js +1 -1
- package/dist/modules/billing/backend/billing/statements/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/topups/page.js +1 -1
- package/dist/modules/billing/backend/billing/topups/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/transactions/page.js +1 -1
- package/dist/modules/billing/backend/billing/transactions/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/usage/create/page.js +3 -2
- package/dist/modules/billing/backend/billing/usage/create/page.js.map +2 -2
- package/dist/modules/billing/backend/config/billing/page.js +472 -0
- package/dist/modules/billing/backend/config/billing/page.js.map +7 -0
- package/dist/modules/billing/backend/config/billing/page.meta.js +19 -0
- package/dist/modules/billing/backend/config/billing/page.meta.js.map +7 -0
- package/dist/modules/billing/cli.js +22 -1
- package/dist/modules/billing/cli.js.map +2 -2
- package/dist/modules/billing/commands/accounts.js +6 -0
- package/dist/modules/billing/commands/accounts.js.map +2 -2
- package/dist/modules/billing/commands/index.js +3 -0
- package/dist/modules/billing/commands/index.js.map +2 -2
- package/dist/modules/billing/commands/invoices.js +269 -167
- package/dist/modules/billing/commands/invoices.js.map +2 -2
- package/dist/modules/billing/commands/items.js +108 -0
- package/dist/modules/billing/commands/items.js.map +2 -2
- package/dist/modules/billing/commands/memos.js +179 -0
- package/dist/modules/billing/commands/memos.js.map +7 -0
- package/dist/modules/billing/commands/payments.js +379 -0
- package/dist/modules/billing/commands/payments.js.map +7 -0
- package/dist/modules/billing/commands/runs.js +151 -4
- package/dist/modules/billing/commands/runs.js.map +2 -2
- package/dist/modules/billing/commands/settings.js +59 -0
- package/dist/modules/billing/commands/settings.js.map +7 -0
- package/dist/modules/billing/commands/topups.js +9 -2
- package/dist/modules/billing/commands/topups.js.map +2 -2
- package/dist/modules/billing/commands/transactions.js +2 -2
- package/dist/modules/billing/commands/transactions.js.map +2 -2
- package/dist/modules/billing/components/AccountForm.js +149 -42
- package/dist/modules/billing/components/AccountForm.js.map +2 -2
- package/dist/modules/billing/components/AccountInvoicesSection.js +64 -0
- package/dist/modules/billing/components/AccountInvoicesSection.js.map +7 -0
- package/dist/modules/billing/components/AccountPaymentsSection.js +43 -0
- package/dist/modules/billing/components/AccountPaymentsSection.js.map +7 -0
- package/dist/modules/billing/components/AccountPicker.js +56 -0
- package/dist/modules/billing/components/AccountPicker.js.map +7 -0
- package/dist/modules/billing/components/AllocatePaymentDialog.js +134 -0
- package/dist/modules/billing/components/AllocatePaymentDialog.js.map +7 -0
- package/dist/modules/billing/components/BillingDataTable.js +21 -0
- package/dist/modules/billing/components/BillingDataTable.js.map +7 -0
- package/dist/modules/billing/components/BillingSettingsLink.js +19 -0
- package/dist/modules/billing/components/BillingSettingsLink.js.map +7 -0
- package/dist/modules/billing/components/ChangeItemPriceDialog.js +158 -0
- package/dist/modules/billing/components/ChangeItemPriceDialog.js.map +7 -0
- package/dist/modules/billing/components/CreateMemoDialog.js +200 -0
- package/dist/modules/billing/components/CreateMemoDialog.js.map +7 -0
- package/dist/modules/billing/components/CustomerLabel.js +33 -0
- package/dist/modules/billing/components/CustomerLabel.js.map +7 -0
- package/dist/modules/billing/components/InvoicesTable.js +270 -0
- package/dist/modules/billing/components/InvoicesTable.js.map +7 -0
- package/dist/modules/billing/components/ItemForm.js +176 -44
- package/dist/modules/billing/components/ItemForm.js.map +2 -2
- package/dist/modules/billing/components/ItemPriceField.js +46 -0
- package/dist/modules/billing/components/ItemPriceField.js.map +7 -0
- package/dist/modules/billing/components/LineFormDialog.js +25 -2
- package/dist/modules/billing/components/LineFormDialog.js.map +2 -2
- package/dist/modules/billing/components/ManualPaymentDialog.js +122 -0
- package/dist/modules/billing/components/ManualPaymentDialog.js.map +7 -0
- package/dist/modules/billing/components/MoneyInput.js +14 -0
- package/dist/modules/billing/components/MoneyInput.js.map +7 -0
- package/dist/modules/billing/components/PaymentUploadDialog.js +115 -0
- package/dist/modules/billing/components/PaymentUploadDialog.js.map +7 -0
- package/dist/modules/billing/components/PaymentsTable.js +171 -0
- package/dist/modules/billing/components/PaymentsTable.js.map +7 -0
- package/dist/modules/billing/components/PostRunDraftsButton.js +105 -0
- package/dist/modules/billing/components/PostRunDraftsButton.js.map +7 -0
- package/dist/modules/billing/components/PrepaidPanel.js +14 -4
- package/dist/modules/billing/components/PrepaidPanel.js.map +2 -2
- package/dist/modules/billing/components/RunCyclesSection.js +209 -0
- package/dist/modules/billing/components/RunCyclesSection.js.map +7 -0
- package/dist/modules/billing/components/TestRunActions.js +127 -0
- package/dist/modules/billing/components/TestRunActions.js.map +7 -0
- package/dist/modules/billing/components/account/AccountItemsTab.js +157 -0
- package/dist/modules/billing/components/account/AccountItemsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountRunBillingDialog.js +122 -0
- package/dist/modules/billing/components/account/AccountRunBillingDialog.js.map +7 -0
- package/dist/modules/billing/components/account/AccountRunsTab.js +152 -0
- package/dist/modules/billing/components/account/AccountRunsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountStatementsTab.js +98 -0
- package/dist/modules/billing/components/account/AccountStatementsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountSummaryCards.js +125 -0
- package/dist/modules/billing/components/account/AccountSummaryCards.js.map +7 -0
- package/dist/modules/billing/components/account/AccountUsageTab.js +91 -0
- package/dist/modules/billing/components/account/AccountUsageTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountView.js +73 -0
- package/dist/modules/billing/components/account/AccountView.js.map +7 -0
- package/dist/modules/billing/components/account/accountRow.js +39 -0
- package/dist/modules/billing/components/account/accountRow.js.map +7 -0
- package/dist/modules/billing/components/customerNames.js +42 -0
- package/dist/modules/billing/components/customerNames.js.map +7 -0
- package/dist/modules/billing/data/entities.js +560 -4
- package/dist/modules/billing/data/entities.js.map +2 -2
- package/dist/modules/billing/data/entityIds.js +10 -1
- package/dist/modules/billing/data/entityIds.js.map +2 -2
- package/dist/modules/billing/data/validators.js +269 -15
- package/dist/modules/billing/data/validators.js.map +2 -2
- package/dist/modules/billing/events.js +50 -0
- package/dist/modules/billing/events.js.map +2 -2
- package/dist/modules/billing/i18n/en.json +457 -21
- package/dist/modules/billing/i18n/pl.json +457 -21
- package/dist/modules/billing/index.js +6 -2
- package/dist/modules/billing/index.js.map +2 -2
- package/dist/modules/billing/lib/accountFinancials.js +58 -0
- package/dist/modules/billing/lib/accountFinancials.js.map +7 -0
- package/dist/modules/billing/lib/accountLock.js +14 -0
- package/dist/modules/billing/lib/accountLock.js.map +7 -0
- package/dist/modules/billing/lib/accountStatus.js +9 -7
- package/dist/modules/billing/lib/accountStatus.js.map +2 -2
- package/dist/modules/billing/lib/accountSummary.js +106 -0
- package/dist/modules/billing/lib/accountSummary.js.map +7 -0
- package/dist/modules/billing/lib/allocation.js +356 -0
- package/dist/modules/billing/lib/allocation.js.map +7 -0
- package/dist/modules/billing/lib/allocationEvents.js +22 -0
- package/dist/modules/billing/lib/allocationEvents.js.map +7 -0
- package/dist/modules/billing/lib/billPeriod.js +15 -1
- package/dist/modules/billing/lib/billPeriod.js.map +2 -2
- package/dist/modules/billing/lib/billRunEngine.js +237 -102
- package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
- package/dist/modules/billing/lib/billingSettings.js +116 -0
- package/dist/modules/billing/lib/billingSettings.js.map +7 -0
- package/dist/modules/billing/lib/commandRoute.js +81 -0
- package/dist/modules/billing/lib/commandRoute.js.map +7 -0
- package/dist/modules/billing/lib/customerLookup.js +97 -0
- package/dist/modules/billing/lib/customerLookup.js.map +7 -0
- package/dist/modules/billing/lib/dateOnly.js +13 -0
- package/dist/modules/billing/lib/dateOnly.js.map +7 -0
- package/dist/modules/billing/lib/documentParties.js +16 -0
- package/dist/modules/billing/lib/documentParties.js.map +7 -0
- package/dist/modules/billing/lib/documentTypes.js +27 -0
- package/dist/modules/billing/lib/documentTypes.js.map +7 -0
- package/dist/modules/billing/lib/dueDate.js +18 -0
- package/dist/modules/billing/lib/dueDate.js.map +7 -0
- package/dist/modules/billing/lib/iban.js +29 -0
- package/dist/modules/billing/lib/iban.js.map +7 -0
- package/dist/modules/billing/lib/invoiceDelivery.js +131 -0
- package/dist/modules/billing/lib/invoiceDelivery.js.map +7 -0
- package/dist/modules/billing/lib/invoiceLanguages.js +16 -0
- package/dist/modules/billing/lib/invoiceLanguages.js.map +7 -0
- package/dist/modules/billing/lib/invoiceWriter.js +107 -99
- package/dist/modules/billing/lib/invoiceWriter.js.map +2 -2
- package/dist/modules/billing/lib/itemBillingState.js +27 -0
- package/dist/modules/billing/lib/itemBillingState.js.map +7 -0
- package/dist/modules/billing/lib/itemPriceChange.js +18 -0
- package/dist/modules/billing/lib/itemPriceChange.js.map +7 -0
- package/dist/modules/billing/lib/itemSelector.js.map +2 -2
- package/dist/modules/billing/lib/lineAmounts.js +32 -0
- package/dist/modules/billing/lib/lineAmounts.js.map +7 -0
- package/dist/modules/billing/lib/memoGuards.js +25 -0
- package/dist/modules/billing/lib/memoGuards.js.map +7 -0
- package/dist/modules/billing/lib/navigation.js +48 -0
- package/dist/modules/billing/lib/navigation.js.map +7 -0
- package/dist/modules/billing/lib/numbering.js +120 -0
- package/dist/modules/billing/lib/numbering.js.map +7 -0
- package/dist/modules/billing/lib/overdue.js +36 -0
- package/dist/modules/billing/lib/overdue.js.map +7 -0
- package/dist/modules/billing/lib/paymentCsv.js +125 -0
- package/dist/modules/billing/lib/paymentCsv.js.map +7 -0
- package/dist/modules/billing/lib/paymentGateway.js +15 -0
- package/dist/modules/billing/lib/paymentGateway.js.map +7 -0
- package/dist/modules/billing/lib/paymentImport.js +164 -0
- package/dist/modules/billing/lib/paymentImport.js.map +7 -0
- package/dist/modules/billing/lib/paymentMatcher.js +36 -0
- package/dist/modules/billing/lib/paymentMatcher.js.map +7 -0
- package/dist/modules/billing/lib/pdf/attachmentStore.js +46 -0
- package/dist/modules/billing/lib/pdf/attachmentStore.js.map +7 -0
- package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js +7 -0
- package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoiceModel.js +194 -0
- package/dist/modules/billing/lib/pdf/invoiceModel.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoicePdf.js +165 -0
- package/dist/modules/billing/lib/pdf/invoicePdf.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoiceTemplate.js +274 -0
- package/dist/modules/billing/lib/pdf/invoiceTemplate.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConfig.js +17 -25
- package/dist/modules/billing/lib/prepaidConfig.js.map +2 -2
- package/dist/modules/billing/lib/prepaidConsume.js +26 -7
- package/dist/modules/billing/lib/prepaidConsume.js.map +2 -2
- package/dist/modules/billing/lib/prepaidPeriodClose.js +277 -158
- package/dist/modules/billing/lib/prepaidPeriodClose.js.map +2 -2
- package/dist/modules/billing/lib/prepaidTaxPoint.js +37 -0
- package/dist/modules/billing/lib/prepaidTaxPoint.js.map +7 -0
- package/dist/modules/billing/lib/seeds/demo.js +51 -3
- package/dist/modules/billing/lib/seeds/demo.js.map +2 -2
- package/dist/modules/billing/lib/seeds/paymentsSample.js +93 -0
- package/dist/modules/billing/lib/seeds/paymentsSample.js.map +7 -0
- package/dist/modules/billing/lib/seeds/sellerSettings.js +41 -0
- package/dist/modules/billing/lib/seeds/sellerSettings.js.map +7 -0
- package/dist/modules/billing/lib/servicePeriods.js +121 -0
- package/dist/modules/billing/lib/servicePeriods.js.map +7 -0
- package/dist/modules/billing/lib/testRunCleanup.js +24 -0
- package/dist/modules/billing/lib/testRunCleanup.js.map +7 -0
- package/dist/modules/billing/lib/topupCapture.js +2 -2
- package/dist/modules/billing/lib/topupCapture.js.map +2 -2
- package/dist/modules/billing/lib/topupReceipt.js +58 -79
- package/dist/modules/billing/lib/topupReceipt.js.map +2 -2
- package/dist/modules/billing/lib/usageRater.js.map +1 -1
- package/dist/modules/billing/lib/usageRunner.js +14 -6
- package/dist/modules/billing/lib/usageRunner.js.map +2 -2
- package/dist/modules/billing/lib/vat.js +39 -0
- package/dist/modules/billing/lib/vat.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js +15 -0
- package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js +25 -0
- package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js +115 -0
- package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js +47 -0
- package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js +89 -0
- package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js +17 -0
- package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js +27 -0
- package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js +29 -0
- package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js.map +7 -0
- package/dist/modules/billing/setup.js +7 -184
- package/dist/modules/billing/setup.js.map +2 -2
- package/dist/modules/billing/subscribers/credit-memo-auto-send.js +16 -0
- package/dist/modules/billing/subscribers/credit-memo-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/debit-memo-apply-credit.js +16 -0
- package/dist/modules/billing/subscribers/debit-memo-apply-credit.js.map +7 -0
- package/dist/modules/billing/subscribers/debit-memo-auto-send.js +16 -0
- package/dist/modules/billing/subscribers/debit-memo-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/invoice-apply-credit.js +34 -0
- package/dist/modules/billing/subscribers/invoice-apply-credit.js.map +7 -0
- package/dist/modules/billing/subscribers/invoice-auto-send.js +30 -0
- package/dist/modules/billing/subscribers/invoice-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js +69 -33
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +2 -2
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js +4 -6
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +2 -2
- package/generated/entities/billing_account/index.ts +3 -0
- package/generated/entities/billing_allocation/index.ts +18 -0
- package/generated/entities/billing_document_sequence/index.ts +11 -0
- package/generated/entities/billing_invoice/index.ts +38 -0
- package/generated/entities/billing_invoice_line/index.ts +24 -0
- package/generated/entities/billing_item/index.ts +4 -0
- package/generated/entities/billing_payment/index.ts +25 -0
- package/generated/entities/billing_payment_import/index.ts +14 -0
- package/generated/entities/billing_run/index.ts +0 -1
- package/generated/entities/billing_run_outcome/index.ts +1 -0
- package/generated/entities/billing_settings/index.ts +26 -0
- package/generated/entities/billing_statement/index.ts +1 -0
- package/generated/entities.ids.generated.ts +7 -0
- package/package.json +5 -1
- package/src/modules/billing/__integration__/TC-BILL-004.spec.ts +170 -0
- package/src/modules/billing/__integration__/TC-BILL-005.spec.ts +134 -0
- package/src/modules/billing/__integration__/TC-BILL-006.spec.ts +101 -0
- package/src/modules/billing/__integration__/TC-BILL-007.spec.ts +140 -0
- package/src/modules/billing/__integration__/TC-BILL-008.spec.ts +108 -0
- package/src/modules/billing/__integration__/TC-BILL-009.spec.ts +113 -0
- package/src/modules/billing/__integration__/TC-BILL-010.spec.ts +118 -0
- package/src/modules/billing/__integration__/TC-BILL-011.spec.ts +142 -0
- package/src/modules/billing/__integration__/TC-BILL-012.spec.ts +115 -0
- package/src/modules/billing/__integration__/TC-BILL-013.spec.ts +118 -0
- package/src/modules/billing/__integration__/sellerSettings.ts +24 -0
- package/src/modules/billing/__tests__/commands/invoices.test.ts +436 -354
- package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +33 -0
- package/src/modules/billing/__tests__/commands/memos.test.ts +205 -0
- package/src/modules/billing/__tests__/commands/payments.test.ts +241 -0
- package/src/modules/billing/__tests__/commands/runsDelete.test.ts +146 -0
- package/src/modules/billing/__tests__/commands/runsPostDrafts.test.ts +106 -0
- package/src/modules/billing/__tests__/commands/topups.test.ts +11 -0
- package/src/modules/billing/__tests__/entityRegistry.test.ts +8 -1
- package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +28 -11
- package/src/modules/billing/__tests__/lib/accountFinancials.test.ts +53 -0
- package/src/modules/billing/__tests__/lib/accountSummary.test.ts +97 -0
- package/src/modules/billing/__tests__/lib/allocation.test.ts +319 -0
- package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +228 -17
- package/src/modules/billing/__tests__/lib/billingSettings.test.ts +109 -0
- package/src/modules/billing/__tests__/lib/customerLookup.test.ts +156 -0
- package/src/modules/billing/__tests__/lib/dueDate.test.ts +35 -0
- package/src/modules/billing/__tests__/lib/iban.test.ts +38 -0
- package/src/modules/billing/__tests__/lib/invoiceDelivery.test.ts +189 -0
- package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +212 -10
- package/src/modules/billing/__tests__/lib/itemBillingState.test.ts +42 -0
- package/src/modules/billing/__tests__/lib/itemPriceChange.test.ts +165 -0
- package/src/modules/billing/__tests__/lib/lineAmounts.test.ts +86 -0
- package/src/modules/billing/__tests__/lib/navigation.test.ts +71 -0
- package/src/modules/billing/__tests__/lib/numbering.test.ts +91 -0
- package/src/modules/billing/__tests__/lib/overdue.test.ts +36 -0
- package/src/modules/billing/__tests__/lib/paymentCsv.test.ts +148 -0
- package/src/modules/billing/__tests__/lib/paymentImport.test.ts +113 -0
- package/src/modules/billing/__tests__/lib/paymentsSample.test.ts +73 -0
- package/src/modules/billing/__tests__/lib/pdf/invoicePdf.test.ts +199 -0
- package/src/modules/billing/__tests__/lib/pdf/invoicePdfService.test.ts +147 -0
- package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +374 -98
- package/src/modules/billing/__tests__/lib/prepaidTaxPoint.test.ts +118 -0
- package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +53 -2
- package/src/modules/billing/__tests__/lib/sellerSettings.test.ts +28 -0
- package/src/modules/billing/__tests__/lib/servicePeriods.test.ts +190 -0
- package/src/modules/billing/__tests__/lib/topupCapture.test.ts +6 -28
- package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +21 -52
- package/src/modules/billing/__tests__/lib/vat.test.ts +51 -0
- package/src/modules/billing/__tests__/package-isolation.test.ts +22 -8
- package/src/modules/billing/__tests__/setup.test.ts +13 -4
- package/src/modules/billing/__tests__/validators.test.ts +193 -3
- package/src/modules/billing/acl.ts +12 -0
- package/src/modules/billing/api/accounts/[id]/summary/route.ts +62 -0
- package/src/modules/billing/api/accounts/route.ts +58 -4
- package/src/modules/billing/api/export/account/[id]/route.ts +13 -11
- package/src/modules/billing/api/invoices/[id]/memos/route.ts +63 -0
- package/src/modules/billing/api/invoices/[id]/pdf/route.ts +79 -0
- package/src/modules/billing/api/invoices/[id]/route.ts +57 -70
- package/src/modules/billing/api/invoices/[id]/send/route.ts +127 -0
- package/src/modules/billing/api/invoices/add-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/delete-draft/route.ts +33 -0
- package/src/modules/billing/api/invoices/edit-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/post/route.ts +7 -6
- package/src/modules/billing/api/invoices/remove-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/route.ts +81 -86
- package/src/modules/billing/api/items/bulk/route.ts +2 -1
- package/src/modules/billing/api/items/change-price/route.ts +54 -0
- package/src/modules/billing/api/items/route.ts +23 -7
- package/src/modules/billing/api/payment-imports/[id]/route.ts +49 -0
- package/src/modules/billing/api/payment-imports/route.ts +57 -0
- package/src/modules/billing/api/payments/[id]/route.ts +63 -0
- package/src/modules/billing/api/payments/allocate/route.ts +35 -0
- package/src/modules/billing/api/payments/assign/route.ts +36 -0
- package/src/modules/billing/api/payments/convert-to-topup/route.ts +35 -0
- package/src/modules/billing/api/payments/ignore/route.ts +35 -0
- package/src/modules/billing/api/payments/import/route.ts +72 -0
- package/src/modules/billing/api/payments/restore/route.ts +35 -0
- package/src/modules/billing/api/payments/route.ts +147 -0
- package/src/modules/billing/api/payments/unallocate/route.ts +35 -0
- package/src/modules/billing/api/payments/unassign/route.ts +35 -0
- package/src/modules/billing/api/run-outcomes/route.ts +49 -6
- package/src/modules/billing/api/runs/[id]/post-drafts/route.ts +71 -0
- package/src/modules/billing/api/runs/[id]/route.ts +59 -0
- package/src/modules/billing/api/runs/retry-failed/route.ts +2 -1
- package/src/modules/billing/api/runs/route.ts +15 -7
- package/src/modules/billing/api/settings/number-preview/route.ts +46 -0
- package/src/modules/billing/api/settings/route.ts +116 -0
- package/src/modules/billing/api/statements/route.ts +5 -0
- package/src/modules/billing/api/test-invoices/wipe/route.ts +2 -2
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +151 -372
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +6 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +70 -4
- package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +444 -95
- package/src/modules/billing/backend/billing/invoices/page.tsx +8 -227
- package/src/modules/billing/backend/billing/items/[id]/page.tsx +75 -6
- package/src/modules/billing/backend/billing/items/create/page.tsx +13 -8
- package/src/modules/billing/backend/billing/items/page.tsx +18 -2
- package/src/modules/billing/backend/billing/payments/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/payments/[id]/page.tsx +415 -0
- package/src/modules/billing/backend/billing/payments/imports/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/payments/imports/[id]/page.tsx +106 -0
- package/src/modules/billing/backend/billing/payments/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/payments/page.tsx +56 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.tsx +49 -18
- package/src/modules/billing/backend/billing/runs/page.tsx +59 -8
- package/src/modules/billing/backend/billing/statements/[id]/page.tsx +86 -5
- package/src/modules/billing/backend/billing/statements/page.tsx +1 -1
- package/src/modules/billing/backend/billing/topups/page.tsx +1 -1
- package/src/modules/billing/backend/billing/transactions/page.tsx +1 -1
- package/src/modules/billing/backend/billing/usage/create/page.tsx +3 -2
- package/src/modules/billing/backend/config/billing/page.meta.ts +21 -0
- package/src/modules/billing/backend/config/billing/page.tsx +549 -0
- package/src/modules/billing/cli.ts +31 -1
- package/src/modules/billing/commands/accounts.ts +6 -0
- package/src/modules/billing/commands/index.ts +3 -0
- package/src/modules/billing/commands/invoices.ts +364 -231
- package/src/modules/billing/commands/items.ts +143 -0
- package/src/modules/billing/commands/memos.ts +218 -0
- package/src/modules/billing/commands/payments.ts +428 -0
- package/src/modules/billing/commands/runs.ts +205 -3
- package/src/modules/billing/commands/settings.ts +77 -0
- package/src/modules/billing/commands/topups.ts +12 -3
- package/src/modules/billing/commands/transactions.ts +2 -2
- package/src/modules/billing/components/AccountForm.tsx +199 -53
- package/src/modules/billing/components/AccountInvoicesSection.tsx +75 -0
- package/src/modules/billing/components/AccountPaymentsSection.tsx +42 -0
- package/src/modules/billing/components/AccountPicker.tsx +70 -0
- package/src/modules/billing/components/AllocatePaymentDialog.tsx +167 -0
- package/src/modules/billing/components/BillingDataTable.tsx +34 -0
- package/src/modules/billing/components/BillingSettingsLink.tsx +25 -0
- package/src/modules/billing/components/ChangeItemPriceDialog.tsx +180 -0
- package/src/modules/billing/components/CreateMemoDialog.tsx +232 -0
- package/src/modules/billing/components/CustomerLabel.tsx +53 -0
- package/src/modules/billing/components/InvoicesTable.tsx +344 -0
- package/src/modules/billing/components/ItemForm.tsx +233 -44
- package/src/modules/billing/components/ItemPriceField.tsx +81 -0
- package/src/modules/billing/components/LineFormDialog.tsx +25 -1
- package/src/modules/billing/components/ManualPaymentDialog.tsx +140 -0
- package/src/modules/billing/components/MoneyInput.tsx +26 -0
- package/src/modules/billing/components/PaymentUploadDialog.tsx +148 -0
- package/src/modules/billing/components/PaymentsTable.tsx +213 -0
- package/src/modules/billing/components/PostRunDraftsButton.tsx +129 -0
- package/src/modules/billing/components/PrepaidPanel.tsx +19 -6
- package/src/modules/billing/components/RunCyclesSection.tsx +298 -0
- package/src/modules/billing/components/TestRunActions.tsx +152 -0
- package/src/modules/billing/components/account/AccountItemsTab.tsx +192 -0
- package/src/modules/billing/components/account/AccountRunBillingDialog.tsx +147 -0
- package/src/modules/billing/components/account/AccountRunsTab.tsx +201 -0
- package/src/modules/billing/components/account/AccountStatementsTab.tsx +120 -0
- package/src/modules/billing/components/account/AccountSummaryCards.tsx +153 -0
- package/src/modules/billing/components/account/AccountUsageTab.tsx +122 -0
- package/src/modules/billing/components/account/AccountView.tsx +101 -0
- package/src/modules/billing/components/account/accountRow.ts +70 -0
- package/src/modules/billing/components/customerNames.ts +62 -0
- package/src/modules/billing/data/entities.ts +654 -5
- package/src/modules/billing/data/entityIds.ts +9 -0
- package/src/modules/billing/data/validators.ts +388 -8
- package/src/modules/billing/events.ts +56 -3
- package/src/modules/billing/i18n/en.json +457 -21
- package/src/modules/billing/i18n/pl.json +457 -21
- package/src/modules/billing/index.ts +6 -2
- package/src/modules/billing/lib/accountFinancials.ts +84 -0
- package/src/modules/billing/lib/accountLock.ts +32 -0
- package/src/modules/billing/lib/accountStatus.ts +11 -9
- package/src/modules/billing/lib/accountSummary.ts +172 -0
- package/src/modules/billing/lib/allocation.ts +515 -0
- package/src/modules/billing/lib/allocationEvents.ts +26 -0
- package/src/modules/billing/lib/billPeriod.ts +19 -0
- package/src/modules/billing/lib/billRunEngine.ts +350 -150
- package/src/modules/billing/lib/billingSettings.ts +145 -0
- package/src/modules/billing/lib/commandRoute.ts +112 -0
- package/src/modules/billing/lib/customerLookup.ts +157 -0
- package/src/modules/billing/lib/dateOnly.ts +22 -0
- package/src/modules/billing/lib/documentParties.ts +23 -0
- package/src/modules/billing/lib/documentTypes.ts +37 -0
- package/src/modules/billing/lib/dueDate.ts +26 -0
- package/src/modules/billing/lib/iban.ts +36 -0
- package/src/modules/billing/lib/invoiceDelivery.ts +197 -0
- package/src/modules/billing/lib/invoiceLanguages.ts +34 -0
- package/src/modules/billing/lib/invoiceWriter.ts +162 -148
- package/src/modules/billing/lib/itemBillingState.ts +67 -0
- package/src/modules/billing/lib/itemPriceChange.ts +56 -0
- package/src/modules/billing/lib/itemSelector.ts +4 -1
- package/src/modules/billing/lib/lineAmounts.ts +61 -0
- package/src/modules/billing/lib/memoGuards.ts +41 -0
- package/src/modules/billing/lib/navigation.ts +64 -0
- package/src/modules/billing/lib/numbering.ts +174 -0
- package/src/modules/billing/lib/overdue.ts +60 -0
- package/src/modules/billing/lib/paymentCsv.ts +171 -0
- package/src/modules/billing/lib/paymentGateway.ts +52 -0
- package/src/modules/billing/lib/paymentImport.ts +228 -0
- package/src/modules/billing/lib/paymentMatcher.ts +88 -0
- package/src/modules/billing/lib/pdf/attachmentStore.ts +60 -0
- package/src/modules/billing/lib/pdf/fonts/NotoSans-Bold.ttf +0 -0
- package/src/modules/billing/lib/pdf/fonts/NotoSans-Regular.ttf +0 -0
- package/src/modules/billing/lib/pdf/fonts/OFL.txt +93 -0
- package/src/modules/billing/lib/pdf/fonts/embeddedFonts.ts +9 -0
- package/src/modules/billing/lib/pdf/invoiceModel.ts +328 -0
- package/src/modules/billing/lib/pdf/invoicePdf.ts +216 -0
- package/src/modules/billing/lib/pdf/invoiceTemplate.ts +338 -0
- package/src/modules/billing/lib/prepaidConfig.ts +27 -36
- package/src/modules/billing/lib/prepaidConsume.ts +29 -6
- package/src/modules/billing/lib/prepaidPeriodClose.ts +407 -239
- package/src/modules/billing/lib/prepaidTaxPoint.ts +76 -0
- package/src/modules/billing/lib/seeds/demo.ts +79 -6
- package/src/modules/billing/lib/seeds/paymentsSample.ts +136 -0
- package/src/modules/billing/lib/seeds/sellerSettings.ts +78 -0
- package/src/modules/billing/lib/servicePeriods.ts +226 -0
- package/src/modules/billing/lib/testRunCleanup.ts +42 -0
- package/src/modules/billing/lib/topupCapture.ts +3 -4
- package/src/modules/billing/lib/topupReceipt.ts +70 -103
- package/src/modules/billing/lib/usageRater.ts +1 -1
- package/src/modules/billing/lib/usageRunner.ts +17 -5
- package/src/modules/billing/lib/vat.ts +56 -0
- package/src/modules/billing/migrations/Migration20261114000000_billing_customer_kind.ts +25 -0
- package/src/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.ts +37 -0
- package/src/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.ts +132 -0
- package/src/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.ts +58 -0
- package/src/modules/billing/migrations/Migration20261117000000_billing_payments.ts +97 -0
- package/src/modules/billing/migrations/Migration20261118000000_billing_memos.ts +21 -0
- package/src/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.ts +39 -0
- package/src/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.ts +35 -0
- package/src/modules/billing/setup.ts +11 -237
- package/src/modules/billing/subscribers/credit-memo-auto-send.ts +15 -0
- package/src/modules/billing/subscribers/debit-memo-apply-credit.ts +15 -0
- package/src/modules/billing/subscribers/debit-memo-auto-send.ts +15 -0
- package/src/modules/billing/subscribers/invoice-apply-credit.ts +39 -0
- package/src/modules/billing/subscribers/invoice-auto-send.ts +46 -0
- package/src/modules/billing/subscribers/payment-captured-to-paid.ts +84 -62
- package/src/modules/billing/subscribers/prepaid-topup-captured.ts +4 -8
- package/dist/modules/billing/lib/invoiceStatus.js +0 -38
- package/dist/modules/billing/lib/invoiceStatus.js.map +0 -7
- package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +0 -97
- package/src/modules/billing/lib/invoiceStatus.ts +0 -61
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/commands/payments.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { BillingAccount, BillingPayment, BillingTopup } from '../data/entities'\nimport {\n billingAllocationRemoveSchema,\n billingPaymentAllocateSchema,\n billingPaymentAssignSchema,\n billingPaymentConvertSchema,\n billingPaymentCreateSchema,\n billingPaymentIdSchema,\n billingPaymentImportSchema,\n type BillingAllocationRemoveInput,\n type BillingPaymentAllocateInput,\n type BillingPaymentAssignInput,\n type BillingPaymentConvertInput,\n type BillingPaymentCreateInput,\n type BillingPaymentIdInput,\n type BillingPaymentImportInput,\n} from '../data/validators'\nimport { emitBillingEvent } from '../events'\nimport { withAccountLock } from '../lib/accountLock'\nimport {\n allocate,\n applyAccountCredit,\n emptyEffects,\n mergeEffects,\n toCents,\n unallocate,\n type AllocationEffects,\n type ArScope,\n} from '../lib/allocation'\nimport { emitAllocationEffects } from '../lib/allocationEvents'\nimport { getBillingSettings, receiptTaxRate } from '../lib/billingSettings'\nimport { importPayments, type ImportSummary } from '../lib/paymentImport'\nimport { captureTopup } from '../lib/topupCapture'\nimport { createHash } from 'node:crypto'\n\n/**\n * Payment commands (Spec C). Every receivables change runs inside the\n * per-account lock (`withAccountLock`); events fire after commit.\n *\n * Reversals are explicit inverse commands, consistent with the rest of the\n * billing module (which does not use the framework undo log): allocate \u2194\n * unallocate, assign \u2194 unassign, ignore \u2194 restore. Convert-to-top-up is\n * terminal (reverse through the prepaid adjustment flow, C-D3).\n */\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction userIdOf(ctx: CommandRuntimeContext): string | null {\n return (ctx.auth?.sub as string | undefined) ?? null\n}\n\nfunction scopeOf(input: { tenantId: string; organizationId: string }): ArScope {\n return { tenantId: input.tenantId, organizationId: input.organizationId }\n}\n\nasync function loadPayment(em: EntityManager, scope: ArScope, paymentId: string): Promise<BillingPayment> {\n const payment = await em.findOne(BillingPayment, { id: paymentId, ...scope, deletedAt: null } as never)\n if (!payment) throw new CrudHttpError(404, { error: 'Payment not found', code: 'billing.payment.not_found' })\n return payment\n}\n\nasync function loadAccount(em: EntityManager, scope: ArScope, billAccountId: string): Promise<BillingAccount> {\n const account = await em.findOne(BillingAccount, { id: billAccountId, ...scope, deletedAt: null } as never)\n if (!account) throw new CrudHttpError(404, { error: 'Billing account not found', code: 'billing.payment.account_missing' })\n return account\n}\n\nasync function countActiveAllocations(em: EntityManager, scope: ArScope, paymentId: string): Promise<number> {\n const rows = (await em.execute(\n `SELECT COUNT(*)::int AS n FROM billing_allocations\n WHERE tenant_id = ? AND organization_id = ? AND source_type = 'payment' AND source_id = ? AND deleted_at IS NULL`,\n [scope.tenantId, scope.organizationId, paymentId],\n )) as unknown as Array<{ n: number }>\n return Number(rows?.[0]?.n ?? 0)\n}\n\nexport type PaymentCommandResult = { paymentId: string; status: string; unallocatedAmount: string }\n\nasync function paymentResult(em: EntityManager, scope: ArScope, paymentId: string): Promise<PaymentCommandResult> {\n const rows = (await em.execute(\n `SELECT status, unallocated_amount FROM billing_payments WHERE id = ? AND tenant_id = ? AND organization_id = ?`,\n [paymentId, scope.tenantId, scope.organizationId],\n )) as unknown as Array<{ status: string; unallocated_amount: string }>\n return { paymentId, status: rows?.[0]?.status ?? 'unmatched', unallocatedAmount: rows?.[0]?.unallocated_amount ?? '0' }\n}\n\n// \u2500\u2500\u2500 Import \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst importPaymentsCommand: CommandHandler<BillingPaymentImportInput, ImportSummary> = {\n id: 'billing.payments.import',\n async execute(rawInput, ctx) {\n const parsed = billingPaymentImportSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = scopeOf(parsed)\n const { summary, effects, paymentIds } = await importPayments(getEm(ctx), {\n scope,\n fileName: parsed.fileName,\n content: parsed.content,\n userId: userIdOf(ctx),\n })\n for (const paymentId of paymentIds) await emitBillingEvent('billing.payment.created', { ...scope, paymentId, source: 'csv' })\n await emitAllocationEffects(scope, effects)\n await emitBillingEvent('billing.payment_import.completed', { ...scope, importId: summary.importId, counts: summary.counts })\n return summary\n },\n}\n\n// \u2500\u2500\u2500 Manual entry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst createPaymentCommand: CommandHandler<BillingPaymentCreateInput, PaymentCommandResult> = {\n id: 'billing.payments.create',\n async execute(rawInput, ctx) {\n const parsed = billingPaymentCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = scopeOf(parsed)\n const em = getEm(ctx)\n if (parsed.billAccountId) {\n const account = await loadAccount(em, scope, parsed.billAccountId)\n if (account.currencyCode !== parsed.currencyCode) {\n throw new CrudHttpError(409, { error: 'Payment currency differs from the account', code: 'billing.payment.currency_mismatch' })\n }\n }\n const amount = parsed.amount.toFixed(4)\n const now = new Date()\n const payment = em.create(BillingPayment, {\n ...scope,\n receivedAt: new Date(`${parsed.receivedAt}T00:00:00Z`),\n amount,\n currencyCode: parsed.currencyCode,\n reference: parsed.reference ?? null,\n payerName: parsed.payerName ?? null,\n payerAccount: parsed.payerAccount ?? null,\n source: 'manual',\n billAccountId: parsed.billAccountId ?? null,\n status: parsed.billAccountId ? 'matched' : 'unmatched',\n unallocatedAmount: amount,\n matchReason: parsed.billAccountId ? null : 'manual',\n sourceRef: `manual:${createHash('sha256').update(`${scope.tenantId}|${now.toISOString()}|${Math.random()}`).digest('hex')}`,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(payment)\n await em.flush()\n let effects = emptyEffects()\n if (parsed.billAccountId) {\n effects = await withAccountLock(em, { tenantId: scope.tenantId, billAccountId: parsed.billAccountId }, (tem) =>\n applyAccountCredit(tem, scope, { id: parsed.billAccountId!, currencyCode: parsed.currencyCode }, {\n onlySources: [{ type: 'payment', id: payment.id }],\n }),\n )\n }\n await emitBillingEvent('billing.payment.created', { ...scope, paymentId: payment.id, source: 'manual' })\n await emitAllocationEffects(scope, effects)\n return paymentResult(em, scope, payment.id)\n },\n}\n\n// \u2500\u2500\u2500 Assign / unassign \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst assignPaymentCommand: CommandHandler<BillingPaymentAssignInput, PaymentCommandResult> = {\n id: 'billing.payments.assign',\n async execute(rawInput, ctx) {\n const parsed = billingPaymentAssignSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = scopeOf(parsed)\n const em = getEm(ctx)\n const [payment, account] = await Promise.all([\n loadPayment(em, scope, parsed.paymentId),\n loadAccount(em, scope, parsed.billAccountId),\n ])\n if (payment.status !== 'unmatched') {\n throw new CrudHttpError(409, { error: 'Only unmatched payments can be assigned', code: 'billing.payment.not_unmatched' })\n }\n if (account.billingMode === 'prepaid') {\n throw new CrudHttpError(409, {\n error: 'Prepaid accounts take payments as top-ups \u2014 use \"Convert to top-up\"',\n code: 'billing.payment.prepaid_account',\n })\n }\n if (account.currencyCode !== payment.currencyCode) {\n throw new CrudHttpError(409, { error: 'Payment currency differs from the account', code: 'billing.payment.currency_mismatch' })\n }\n const effects = await withAccountLock(em, { tenantId: scope.tenantId, billAccountId: account.id }, async (tem) => {\n await tem.execute(\n `UPDATE billing_payments SET bill_account_id = ?, status = 'matched', match_reason = 'manual', updated_at = now()\n WHERE id = ? AND tenant_id = ? AND organization_id = ? AND status = 'unmatched'`,\n [account.id, payment.id, scope.tenantId, scope.organizationId],\n )\n return applyAccountCredit(tem, scope, { id: account.id, currencyCode: account.currencyCode }, {\n onlySources: [{ type: 'payment', id: payment.id }],\n })\n })\n await emitAllocationEffects(scope, effects)\n return paymentResult(em, scope, payment.id)\n },\n}\n\nconst unassignPaymentCommand: CommandHandler<BillingPaymentIdInput, PaymentCommandResult> = {\n id: 'billing.payments.unassign',\n async execute(rawInput, ctx) {\n const parsed = billingPaymentIdSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = scopeOf(parsed)\n const em = getEm(ctx)\n const payment = await loadPayment(em, scope, parsed.paymentId)\n if (payment.status !== 'matched' || !payment.billAccountId) {\n throw new CrudHttpError(409, { error: 'Only fully unallocated matched payments can be unassigned', code: 'billing.payment.not_matched' })\n }\n if ((await countActiveAllocations(em, scope, payment.id)) > 0) {\n throw new CrudHttpError(409, { error: 'Un-allocate the payment first', code: 'billing.payment.has_allocations' })\n }\n await em.execute(\n `UPDATE billing_payments SET bill_account_id = NULL, status = 'unmatched', match_reason = 'manual', updated_at = now()\n WHERE id = ? AND tenant_id = ? AND organization_id = ?`,\n [payment.id, scope.tenantId, scope.organizationId],\n )\n return paymentResult(em, scope, payment.id)\n },\n}\n\n// \u2500\u2500\u2500 Allocate / unallocate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst allocatePaymentCommand: CommandHandler<BillingPaymentAllocateInput, PaymentCommandResult> = {\n id: 'billing.payments.allocate',\n async execute(rawInput, ctx) {\n const parsed = billingPaymentAllocateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = scopeOf(parsed)\n const em = getEm(ctx)\n const payment = await loadPayment(em, scope, parsed.paymentId)\n if (!payment.billAccountId) {\n throw new CrudHttpError(409, { error: 'Assign the payment to an account first', code: 'billing.payment.unassigned' })\n }\n const effects = await withAccountLock(em, { tenantId: scope.tenantId, billAccountId: payment.billAccountId }, async (tem) => {\n const all = emptyEffects()\n for (const entry of parsed.allocations) {\n mergeEffects(\n all,\n await allocate(tem, scope, {\n source: { type: 'payment', id: payment.id },\n invoiceId: entry.invoiceId,\n amountCents: toCents(entry.amount),\n by: 'user',\n userId: userIdOf(ctx),\n }),\n )\n }\n return all\n })\n await emitAllocationEffects(scope, effects)\n return paymentResult(em, scope, payment.id)\n },\n}\n\n/** Source-agnostic: removes a payment OR credit memo allocation (Spec D P-D10). */\nconst unallocateCommand: CommandHandler<BillingAllocationRemoveInput, { allocationId: string; effects: AllocationEffects }> = {\n id: 'billing.payments.unallocate',\n async execute(rawInput, ctx) {\n const parsed = billingAllocationRemoveSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = scopeOf(parsed)\n const em = getEm(ctx)\n const rows = (await em.execute(\n `SELECT bill_account_id FROM billing_allocations\n WHERE id = ? AND tenant_id = ? AND organization_id = ? AND deleted_at IS NULL`,\n [parsed.allocationId, scope.tenantId, scope.organizationId],\n )) as unknown as Array<{ bill_account_id: string }>\n const billAccountId = rows?.[0]?.bill_account_id\n if (!billAccountId) throw new CrudHttpError(404, { error: 'Allocation not found', code: 'billing.allocation.not_found' })\n const effects = await withAccountLock(em, { tenantId: scope.tenantId, billAccountId }, (tem) =>\n unallocate(tem, scope, { allocationId: parsed.allocationId, userId: userIdOf(ctx) }),\n )\n await emitAllocationEffects(scope, effects)\n return { allocationId: parsed.allocationId, effects }\n },\n}\n\n// \u2500\u2500\u2500 Ignore / restore \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ignorePaymentCommand: CommandHandler<BillingPaymentIdInput, PaymentCommandResult> = {\n id: 'billing.payments.ignore',\n async execute(rawInput, ctx) {\n const parsed = billingPaymentIdSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = scopeOf(parsed)\n const em = getEm(ctx)\n const payment = await loadPayment(em, scope, parsed.paymentId)\n if (!['unmatched', 'matched'].includes(payment.status) || (await countActiveAllocations(em, scope, payment.id)) > 0) {\n throw new CrudHttpError(409, { error: 'Only unallocated payments can be ignored', code: 'billing.payment.has_allocations' })\n }\n await em.execute(\n `UPDATE billing_payments SET status = 'ignored', metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object('ignored_from', ?::text), updated_at = now()\n WHERE id = ? AND tenant_id = ? AND organization_id = ?`,\n [payment.status, payment.id, scope.tenantId, scope.organizationId],\n )\n return paymentResult(em, scope, payment.id)\n },\n}\n\nconst restorePaymentCommand: CommandHandler<BillingPaymentIdInput, PaymentCommandResult> = {\n id: 'billing.payments.restore',\n async execute(rawInput, ctx) {\n const parsed = billingPaymentIdSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = scopeOf(parsed)\n const em = getEm(ctx)\n const payment = await loadPayment(em, scope, parsed.paymentId)\n if (payment.status !== 'ignored') {\n throw new CrudHttpError(409, { error: 'Only ignored payments can be restored', code: 'billing.payment.not_ignored' })\n }\n const status = payment.billAccountId ? 'matched' : 'unmatched'\n let effects = emptyEffects()\n if (payment.billAccountId) {\n const account = await loadAccount(em, scope, payment.billAccountId)\n effects = await withAccountLock(em, { tenantId: scope.tenantId, billAccountId: account.id }, async (tem) => {\n await tem.execute(`UPDATE billing_payments SET status = ?, updated_at = now() WHERE id = ?`, [status, payment.id])\n return applyAccountCredit(tem, scope, { id: account.id, currencyCode: account.currencyCode }, {\n onlySources: [{ type: 'payment', id: payment.id }],\n })\n })\n } else {\n await em.execute(`UPDATE billing_payments SET status = ?, updated_at = now() WHERE id = ?`, [status, payment.id])\n }\n await emitAllocationEffects(scope, effects)\n return paymentResult(em, scope, payment.id)\n },\n}\n\n// \u2500\u2500\u2500 Convert to top-up (C-D3) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst convertToTopupCommand: CommandHandler<BillingPaymentConvertInput, PaymentCommandResult & { topupId: string }> = {\n id: 'billing.payments.convert_to_topup',\n async execute(rawInput, ctx) {\n const parsed = billingPaymentConvertSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = scopeOf(parsed)\n const em = getEm(ctx)\n const [payment, account] = await Promise.all([\n loadPayment(em, scope, parsed.paymentId),\n loadAccount(em, scope, parsed.billAccountId),\n ])\n if (account.billingMode !== 'prepaid') {\n throw new CrudHttpError(409, { error: 'Only prepaid accounts take top-ups', code: 'billing.payment.not_prepaid' })\n }\n if (account.currencyCode !== payment.currencyCode) {\n throw new CrudHttpError(409, { error: 'Payment currency differs from the account', code: 'billing.payment.currency_mismatch' })\n }\n const sourceRef = `topup-${payment.id}`\n if (payment.status === 'converted_to_topup' && payment.topupId) {\n return { ...(await paymentResult(em, scope, payment.id)), topupId: payment.topupId }\n }\n if (payment.status !== 'unmatched' || toCents(payment.unallocatedAmount) !== toCents(payment.amount)) {\n throw new CrudHttpError(409, { error: 'Only unmatched, unallocated payments can be converted', code: 'billing.payment.not_convertible' })\n }\n let topup = await em.findOne(BillingTopup, { ...scope, sourceRef } as never)\n if (!topup) {\n const now = new Date()\n topup = em.create(BillingTopup, {\n ...scope,\n billAccountId: account.id,\n status: 'pending',\n amount: payment.amount,\n currencyCode: payment.currencyCode,\n providerKey: 'bank_transfer',\n paymentId: payment.id,\n sourceRef,\n metadata: { billing_payment_id: payment.id, reference: payment.reference ?? null },\n createdAt: now,\n updatedAt: now,\n } as never)\n em.persist(topup)\n await em.flush()\n }\n const settings = await getBillingSettings(em, scope)\n await captureTopup(em, {\n topup,\n account: { id: account.id, organizationId: account.organizationId, tenantId: account.tenantId, currencyCode: account.currencyCode },\n taxRate: receiptTaxRate(settings),\n gatewayTransactionId: payment.reference ?? null,\n })\n await em.execute(\n `UPDATE billing_payments\n SET status = 'converted_to_topup', bill_account_id = ?, topup_id = ?, unallocated_amount = 0, updated_at = now()\n WHERE id = ? AND tenant_id = ? AND organization_id = ?`,\n [account.id, topup.id, payment.id, scope.tenantId, scope.organizationId],\n )\n await emitBillingEvent('billing.payment.converted_to_topup', { ...scope, paymentId: payment.id, topupId: topup.id, billAccountId: account.id })\n return { ...(await paymentResult(em, scope, payment.id)), topupId: topup.id }\n },\n}\n\nregisterCommand(importPaymentsCommand)\nregisterCommand(createPaymentCommand)\nregisterCommand(assignPaymentCommand)\nregisterCommand(unassignPaymentCommand)\nregisterCommand(allocatePaymentCommand)\nregisterCommand(unallocateCommand)\nregisterCommand(ignorePaymentCommand)\nregisterCommand(restorePaymentCommand)\nregisterCommand(convertToTopupCommand)\n\nexport {\n importPaymentsCommand,\n createPaymentCommand,\n assignPaymentCommand,\n unassignPaymentCommand,\n allocatePaymentCommand,\n unallocateCommand,\n ignorePaymentCommand,\n restorePaymentCommand,\n convertToTopupCommand,\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -5,11 +5,16 @@ import {
|
|
|
5
5
|
} from "@open-mercato/shared/lib/commands/scope";
|
|
6
6
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
7
7
|
import { BillingRun, BillingRunOutcome } from "../data/entities.js";
|
|
8
|
+
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
8
9
|
import {
|
|
10
|
+
billingRunDeleteSchema,
|
|
11
|
+
billingRunPostDraftsSchema,
|
|
9
12
|
billingRunRetryFailedSchema,
|
|
10
13
|
billingRunTriggerSchema
|
|
11
14
|
} from "../data/validators.js";
|
|
15
|
+
import { emitBillingEvent } from "../events.js";
|
|
12
16
|
import { runBillRun } from "../lib/billRunEngine.js";
|
|
17
|
+
import { deleteTestRunArtifacts } from "../lib/testRunCleanup.js";
|
|
13
18
|
import { TenantLockBusyError } from "../lib/tenantLock.js";
|
|
14
19
|
function getEm(ctx) {
|
|
15
20
|
return ctx.container.resolve("em").fork();
|
|
@@ -35,8 +40,7 @@ const triggerRunCommand = {
|
|
|
35
40
|
asOfDate,
|
|
36
41
|
triggeredBy: "manual",
|
|
37
42
|
mode: parsed.mode,
|
|
38
|
-
scopedAccountIds: parsed.scopedAccountIds
|
|
39
|
-
catchUp: parsed.catchUp
|
|
43
|
+
scopedAccountIds: parsed.scopedAccountIds
|
|
40
44
|
});
|
|
41
45
|
return {
|
|
42
46
|
billRunId: billRun.id,
|
|
@@ -93,8 +97,7 @@ const retryFailedCommand = {
|
|
|
93
97
|
triggeredBy: "manual",
|
|
94
98
|
mode: parsed.mode,
|
|
95
99
|
scopedAccountIds,
|
|
96
|
-
parentRunId: parentRun.id
|
|
97
|
-
catchUp: parentRun.catchUp
|
|
100
|
+
parentRunId: parentRun.id
|
|
98
101
|
});
|
|
99
102
|
return {
|
|
100
103
|
billRunId: billRun.id,
|
|
@@ -114,10 +117,154 @@ const retryFailedCommand = {
|
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
119
|
};
|
|
120
|
+
function runMode(run) {
|
|
121
|
+
if (run.dryRun) return "dry";
|
|
122
|
+
if (run.testMode) return "test";
|
|
123
|
+
return "real";
|
|
124
|
+
}
|
|
125
|
+
const deleteRunCommand = {
|
|
126
|
+
id: "billing.runs.delete",
|
|
127
|
+
async execute(rawInput, ctx) {
|
|
128
|
+
const parsed = billingRunDeleteSchema.parse(rawInput);
|
|
129
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
130
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
131
|
+
const em = getEm(ctx);
|
|
132
|
+
const outcome = await em.transactional(async (tem) => {
|
|
133
|
+
const run = await tem.findOne(BillingRun, {
|
|
134
|
+
id: parsed.billRunId,
|
|
135
|
+
tenantId: parsed.tenantId,
|
|
136
|
+
organizationId: parsed.organizationId,
|
|
137
|
+
deletedAt: null
|
|
138
|
+
});
|
|
139
|
+
if (!run) throw new CrudHttpError(404, { error: "Bill Run not found", code: "billing.run.not_found" });
|
|
140
|
+
const mode = runMode(run);
|
|
141
|
+
if (mode === "real") {
|
|
142
|
+
throw new CrudHttpError(422, {
|
|
143
|
+
error: "Real Bill Runs cannot be deleted",
|
|
144
|
+
code: "billing.run.delete_real_forbidden"
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (run.status === "running") {
|
|
148
|
+
throw new CrudHttpError(409, { error: "The Bill Run is still running", code: "billing.run.running" });
|
|
149
|
+
}
|
|
150
|
+
const scope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId };
|
|
151
|
+
const invoicesRemoved = await deleteTestRunArtifacts(tem, { ...scope, billRunId: run.id });
|
|
152
|
+
await tem.execute(
|
|
153
|
+
"UPDATE billing_runs SET parent_run_id = NULL WHERE tenant_id = ? AND organization_id = ? AND parent_run_id = ?",
|
|
154
|
+
[scope.tenantId, scope.organizationId, run.id]
|
|
155
|
+
);
|
|
156
|
+
await tem.execute(
|
|
157
|
+
"DELETE FROM billing_run_outcomes WHERE tenant_id = ? AND organization_id = ? AND bill_run_id = ?",
|
|
158
|
+
[scope.tenantId, scope.organizationId, run.id]
|
|
159
|
+
);
|
|
160
|
+
await tem.execute("DELETE FROM billing_runs WHERE tenant_id = ? AND organization_id = ? AND id = ?", [
|
|
161
|
+
scope.tenantId,
|
|
162
|
+
scope.organizationId,
|
|
163
|
+
run.id
|
|
164
|
+
]);
|
|
165
|
+
return { mode, invoicesRemoved };
|
|
166
|
+
});
|
|
167
|
+
await emitBillingEvent(
|
|
168
|
+
"billing.run.deleted",
|
|
169
|
+
{
|
|
170
|
+
billRunId: parsed.billRunId,
|
|
171
|
+
tenantId: parsed.tenantId,
|
|
172
|
+
organizationId: parsed.organizationId,
|
|
173
|
+
mode: outcome.mode,
|
|
174
|
+
invoicesRemoved: outcome.invoicesRemoved
|
|
175
|
+
},
|
|
176
|
+
{ persistent: true }
|
|
177
|
+
);
|
|
178
|
+
return { ok: true, invoicesRemoved: outcome.invoicesRemoved };
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const POST_DRAFTS_DEFAULT_LIMIT = 25;
|
|
182
|
+
const DRAFTS_AFTER_SQL = `SELECT id, number FROM billing_invoices
|
|
183
|
+
WHERE tenant_id = ? AND organization_id = ? AND bill_run_id = ?
|
|
184
|
+
AND status = 'draft' AND test_run = false AND document_type = 'invoice' AND deleted_at IS NULL
|
|
185
|
+
AND (?::uuid IS NULL OR id > ?::uuid)
|
|
186
|
+
ORDER BY id
|
|
187
|
+
LIMIT ?`;
|
|
188
|
+
const DRAFTS_REMAINING_SQL = `SELECT COUNT(*)::int AS remaining FROM billing_invoices
|
|
189
|
+
WHERE tenant_id = ? AND organization_id = ? AND bill_run_id = ?
|
|
190
|
+
AND status = 'draft' AND test_run = false AND document_type = 'invoice' AND deleted_at IS NULL
|
|
191
|
+
AND (?::uuid IS NULL OR id > ?::uuid)`;
|
|
192
|
+
const postDraftsCommand = {
|
|
193
|
+
id: "billing.runs.post_drafts",
|
|
194
|
+
async execute(rawInput, ctx) {
|
|
195
|
+
const parsed = billingRunPostDraftsSchema.parse(rawInput);
|
|
196
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
197
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
198
|
+
const em = getEm(ctx);
|
|
199
|
+
const run = await em.findOne(BillingRun, {
|
|
200
|
+
id: parsed.billRunId,
|
|
201
|
+
tenantId: parsed.tenantId,
|
|
202
|
+
organizationId: parsed.organizationId,
|
|
203
|
+
deletedAt: null
|
|
204
|
+
});
|
|
205
|
+
if (!run) throw new CrudHttpError(404, { error: "Bill Run not found", code: "billing.run.not_found" });
|
|
206
|
+
if (runMode(run) !== "real") {
|
|
207
|
+
throw new CrudHttpError(422, { error: "Only real Bill Runs have invoices to post", code: "billing.run.not_real" });
|
|
208
|
+
}
|
|
209
|
+
if (run.status === "running") {
|
|
210
|
+
throw new CrudHttpError(409, { error: "The Bill Run is still running", code: "billing.run.running" });
|
|
211
|
+
}
|
|
212
|
+
const scope = [parsed.tenantId, parsed.organizationId, run.id];
|
|
213
|
+
const cursor = parsed.afterInvoiceId ?? null;
|
|
214
|
+
const limit = parsed.limit ?? POST_DRAFTS_DEFAULT_LIMIT;
|
|
215
|
+
const drafts = await em.execute(DRAFTS_AFTER_SQL, [...scope, cursor, cursor, limit]);
|
|
216
|
+
const commandBus = ctx.container.resolve("commandBus");
|
|
217
|
+
const failed = [];
|
|
218
|
+
let posted = 0;
|
|
219
|
+
let nextCursor = null;
|
|
220
|
+
let stoppedReason = null;
|
|
221
|
+
for (const draft of drafts ?? []) {
|
|
222
|
+
nextCursor = draft.id;
|
|
223
|
+
try {
|
|
224
|
+
await commandBus.execute("billing.invoices.post", {
|
|
225
|
+
input: { tenantId: parsed.tenantId, organizationId: parsed.organizationId, invoiceId: draft.id },
|
|
226
|
+
ctx
|
|
227
|
+
});
|
|
228
|
+
posted += 1;
|
|
229
|
+
} catch (err) {
|
|
230
|
+
const body = isCrudHttpError(err) ? err.body : null;
|
|
231
|
+
const code = body?.code ?? null;
|
|
232
|
+
failed.push({
|
|
233
|
+
invoiceId: draft.id,
|
|
234
|
+
number: draft.number,
|
|
235
|
+
error: body?.error ?? (err instanceof Error ? err.message : String(err)),
|
|
236
|
+
code
|
|
237
|
+
});
|
|
238
|
+
if (code === "billing.settings.incomplete") {
|
|
239
|
+
stoppedReason = code;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const remainingRows = await em.execute(DRAFTS_REMAINING_SQL, [
|
|
245
|
+
...scope,
|
|
246
|
+
stoppedReason ? cursor : nextCursor,
|
|
247
|
+
stoppedReason ? cursor : nextCursor
|
|
248
|
+
]);
|
|
249
|
+
return {
|
|
250
|
+
posted,
|
|
251
|
+
failed,
|
|
252
|
+
nextCursor: stoppedReason ? cursor : nextCursor,
|
|
253
|
+
remaining: Number(remainingRows?.[0]?.remaining ?? 0),
|
|
254
|
+
stoppedReason
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
};
|
|
117
258
|
registerCommand(triggerRunCommand);
|
|
118
259
|
registerCommand(retryFailedCommand);
|
|
260
|
+
registerCommand(deleteRunCommand);
|
|
261
|
+
registerCommand(postDraftsCommand);
|
|
119
262
|
export {
|
|
263
|
+
POST_DRAFTS_DEFAULT_LIMIT,
|
|
264
|
+
deleteRunCommand,
|
|
265
|
+
postDraftsCommand,
|
|
120
266
|
retryFailedCommand,
|
|
267
|
+
runMode,
|
|
121
268
|
triggerRunCommand
|
|
122
269
|
};
|
|
123
270
|
//# sourceMappingURL=runs.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/commands/runs.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { BillingRun, BillingRunOutcome } from '../data/entities'\nimport {\n billingRunRetryFailedSchema,\n billingRunTriggerSchema,\n type BillingRunRetryFailedInput,\n type BillingRunTriggerInput,\n} from '../data/validators'\nimport { runBillRun } from '../lib/billRunEngine'\nimport { TenantLockBusyError } from '../lib/tenantLock'\n\n/**\n * Bill Run trigger commands.\n *\n * Both commands wrap `runBillRun(...)` from `lib/billRunEngine`. The\n * lock-busy condition (concurrent run on the same tenant) is mapped to\n * HTTP 409 here so the API surface returns the canonical Conflict\n * response without each route knowing the lock primitive's\n * implementation.\n *\n * `triggeredBy` is intentionally NOT exposed on the input schema \u2014\n * client-driven triggers are always `'manual'`. The scheduler worker\n * (Phase 4) injects `'schedule'` directly through a separate\n * orchestrator path, not through this command.\n */\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction getContainer(ctx: CommandRuntimeContext): AwilixContainer {\n return ctx.container as AwilixContainer\n}\n\nfunction asLockBusyError(err: unknown): err is TenantLockBusyError {\n return err instanceof TenantLockBusyError\n}\n\n// \u2500\u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype TriggerResult = {\n billRunId: string\n status: string\n outcomesCount: number\n}\n\nconst triggerRunCommand: CommandHandler<BillingRunTriggerInput, TriggerResult> = {\n id: 'billing.runs.trigger',\n\n async execute(rawInput, ctx) {\n const parsed = billingRunTriggerSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const asOfDate = parsed.asOfDate ?? new Date()\n\n try {\n const { billRun, outcomes } = await runBillRun(em, getContainer(ctx), {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n asOfDate,\n triggeredBy: 'manual',\n mode: parsed.mode,\n scopedAccountIds: parsed.scopedAccountIds,\n catchUp: parsed.catchUp,\n })\n return {\n billRunId: billRun.id,\n status: billRun.status,\n outcomesCount: outcomes.length,\n }\n } catch (err) {\n if (asLockBusyError(err)) {\n // The route metadata maps `CrudHttpError(409)` to the canonical\n // 409 Conflict response. The error body carries the\n // `error_lock_busy` discriminator so clients can distinguish\n // \"lock busy\" from other 409 conditions.\n throw new CrudHttpError(409, {\n error: 'Another Bill Run is already in progress for this tenant',\n code: 'billing.run.lock_busy',\n tenantId: err.tenantId,\n lockName: err.lockName,\n })\n }\n throw err\n }\n },\n}\n\n// \u2500\u2500\u2500 Retry failed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst retryFailedCommand: CommandHandler<BillingRunRetryFailedInput, TriggerResult> = {\n id: 'billing.runs.retry_failed',\n\n async execute(rawInput, ctx) {\n const parsed = billingRunRetryFailedSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const parentRun = await em.findOne(BillingRun, {\n id: parsed.billRunId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!parentRun) {\n throw new CrudHttpError(404, { error: 'Parent Bill Run not found' })\n }\n const failedOutcomes = await em.find(BillingRunOutcome, {\n billRunId: parentRun.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n status: 'failed',\n })\n if (failedOutcomes.length === 0) {\n throw new CrudHttpError(409, {\n error: 'Parent run has no failed outcomes to retry',\n code: 'billing.run.nothing_to_retry',\n })\n }\n const scopedAccountIds = Array.from(new Set(failedOutcomes.map((o) => o.billAccountId)))\n\n try {\n const { billRun, outcomes } = await runBillRun(em, getContainer(ctx), {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n asOfDate: parentRun.asOfDate,\n triggeredBy: 'manual',\n mode: parsed.mode,\n scopedAccountIds,\n parentRunId: parentRun.id,\n catchUp: parentRun.catchUp,\n })\n return {\n billRunId: billRun.id,\n status: billRun.status,\n outcomesCount: outcomes.length,\n }\n } catch (err) {\n if (asLockBusyError(err)) {\n throw new CrudHttpError(409, {\n error: 'Another Bill Run is already in progress for this tenant',\n code: 'billing.run.lock_busy',\n tenantId: err.tenantId,\n lockName: err.lockName,\n })\n }\n throw err\n }\n },\n}\n\nregisterCommand(triggerRunCommand)\nregisterCommand(retryFailedCommand)\n\nexport { triggerRunCommand, retryFailedCommand }\n"],
|
|
5
|
-
"mappings": "AAGA,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAC9B,SAAS,YAAY,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { BillingRun, BillingRunOutcome } from '../data/entities'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n billingRunDeleteSchema,\n billingRunPostDraftsSchema,\n type BillingRunPostDraftsInput,\n billingRunRetryFailedSchema,\n billingRunTriggerSchema,\n type BillingRunDeleteInput,\n type BillingRunRetryFailedInput,\n type BillingRunTriggerInput,\n} from '../data/validators'\nimport { emitBillingEvent } from '../events'\nimport { runBillRun } from '../lib/billRunEngine'\nimport { deleteTestRunArtifacts } from '../lib/testRunCleanup'\nimport { TenantLockBusyError } from '../lib/tenantLock'\n\n/**\n * Bill Run trigger commands.\n *\n * Both commands wrap `runBillRun(...)` from `lib/billRunEngine`. The\n * lock-busy condition (concurrent run on the same tenant) is mapped to\n * HTTP 409 here so the API surface returns the canonical Conflict\n * response without each route knowing the lock primitive's\n * implementation.\n *\n * `triggeredBy` is intentionally NOT exposed on the input schema \u2014\n * client-driven triggers are always `'manual'`. The scheduler worker\n * (Phase 4) injects `'schedule'` directly through a separate\n * orchestrator path, not through this command.\n */\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction getContainer(ctx: CommandRuntimeContext): AwilixContainer {\n return ctx.container as AwilixContainer\n}\n\nfunction asLockBusyError(err: unknown): err is TenantLockBusyError {\n return err instanceof TenantLockBusyError\n}\n\n// \u2500\u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype TriggerResult = {\n billRunId: string\n status: string\n outcomesCount: number\n}\n\nconst triggerRunCommand: CommandHandler<BillingRunTriggerInput, TriggerResult> = {\n id: 'billing.runs.trigger',\n\n async execute(rawInput, ctx) {\n const parsed = billingRunTriggerSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const asOfDate = parsed.asOfDate ?? new Date()\n\n try {\n const { billRun, outcomes } = await runBillRun(em, getContainer(ctx), {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n asOfDate,\n triggeredBy: 'manual',\n mode: parsed.mode,\n scopedAccountIds: parsed.scopedAccountIds,\n })\n return {\n billRunId: billRun.id,\n status: billRun.status,\n outcomesCount: outcomes.length,\n }\n } catch (err) {\n if (asLockBusyError(err)) {\n // The route metadata maps `CrudHttpError(409)` to the canonical\n // 409 Conflict response. The error body carries the\n // `error_lock_busy` discriminator so clients can distinguish\n // \"lock busy\" from other 409 conditions.\n throw new CrudHttpError(409, {\n error: 'Another Bill Run is already in progress for this tenant',\n code: 'billing.run.lock_busy',\n tenantId: err.tenantId,\n lockName: err.lockName,\n })\n }\n throw err\n }\n },\n}\n\n// \u2500\u2500\u2500 Retry failed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst retryFailedCommand: CommandHandler<BillingRunRetryFailedInput, TriggerResult> = {\n id: 'billing.runs.retry_failed',\n\n async execute(rawInput, ctx) {\n const parsed = billingRunRetryFailedSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const parentRun = await em.findOne(BillingRun, {\n id: parsed.billRunId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!parentRun) {\n throw new CrudHttpError(404, { error: 'Parent Bill Run not found' })\n }\n const failedOutcomes = await em.find(BillingRunOutcome, {\n billRunId: parentRun.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n status: 'failed',\n })\n if (failedOutcomes.length === 0) {\n throw new CrudHttpError(409, {\n error: 'Parent run has no failed outcomes to retry',\n code: 'billing.run.nothing_to_retry',\n })\n }\n const scopedAccountIds = Array.from(new Set(failedOutcomes.map((o) => o.billAccountId)))\n\n try {\n const { billRun, outcomes } = await runBillRun(em, getContainer(ctx), {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n asOfDate: parentRun.asOfDate,\n triggeredBy: 'manual',\n mode: parsed.mode,\n scopedAccountIds,\n parentRunId: parentRun.id,\n })\n return {\n billRunId: billRun.id,\n status: billRun.status,\n outcomesCount: outcomes.length,\n }\n } catch (err) {\n if (asLockBusyError(err)) {\n throw new CrudHttpError(409, {\n error: 'Another Bill Run is already in progress for this tenant',\n code: 'billing.run.lock_busy',\n tenantId: err.tenantId,\n lockName: err.lockName,\n })\n }\n throw err\n }\n },\n}\n\n// \u2500\u2500\u2500 Delete (Spec F, F-D1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Test and dry runs are scratch data: deleting one removes its test\n// invoices (with lines and draft-edit audit rows), its outcomes and the run\n// row, in one transaction. Real runs are the audit trail and are refused.\n// Retry children keep existing; their parent link is cleared. Not undoable,\n// like the test-invoice wipe.\n\ntype DeleteRunResult = { ok: true; invoicesRemoved: number }\n\nexport function runMode(run: Pick<BillingRun, 'dryRun' | 'testMode'>): 'real' | 'test' | 'dry' {\n if (run.dryRun) return 'dry'\n if (run.testMode) return 'test'\n return 'real'\n}\n\nconst deleteRunCommand: CommandHandler<BillingRunDeleteInput, DeleteRunResult> = {\n id: 'billing.runs.delete',\n\n async execute(rawInput, ctx) {\n const parsed = billingRunDeleteSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const outcome = await em.transactional(async (tem) => {\n const run = await tem.findOne(BillingRun, {\n id: parsed.billRunId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n // 404 for another organization's run too (no existence leak).\n if (!run) throw new CrudHttpError(404, { error: 'Bill Run not found', code: 'billing.run.not_found' })\n const mode = runMode(run)\n if (mode === 'real') {\n throw new CrudHttpError(422, {\n error: 'Real Bill Runs cannot be deleted',\n code: 'billing.run.delete_real_forbidden',\n })\n }\n if (run.status === 'running') {\n throw new CrudHttpError(409, { error: 'The Bill Run is still running', code: 'billing.run.running' })\n }\n const scope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n const invoicesRemoved = await deleteTestRunArtifacts(tem as EntityManager, { ...scope, billRunId: run.id })\n await tem.execute(\n 'UPDATE billing_runs SET parent_run_id = NULL WHERE tenant_id = ? AND organization_id = ? AND parent_run_id = ?',\n [scope.tenantId, scope.organizationId, run.id],\n )\n await tem.execute(\n 'DELETE FROM billing_run_outcomes WHERE tenant_id = ? AND organization_id = ? AND bill_run_id = ?',\n [scope.tenantId, scope.organizationId, run.id],\n )\n await tem.execute('DELETE FROM billing_runs WHERE tenant_id = ? AND organization_id = ? AND id = ?', [\n scope.tenantId,\n scope.organizationId,\n run.id,\n ])\n return { mode, invoicesRemoved }\n })\n\n // After commit: subscribers never see a phantom delete.\n await emitBillingEvent(\n 'billing.run.deleted',\n {\n billRunId: parsed.billRunId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n mode: outcome.mode,\n invoicesRemoved: outcome.invoicesRemoved,\n },\n { persistent: true },\n )\n return { ok: true, invoicesRemoved: outcome.invoicesRemoved }\n },\n}\n\n// \u2500\u2500\u2500 Post the run's drafts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Posts the draft invoices a REAL run wrote, in bounded batches (ascending\n// id, keyset cursor) so a run with thousands of invoices never becomes one\n// long request. Each invoice goes through `billing.invoices.post` on its own\n// \u2014 numbering, frozen PDF, due date, credit auto-apply, auto-send and events\n// stay exactly as for a single post \u2014 so one failure never undoes the\n// others. A seller-settings failure would fail every invoice, so the batch\n// stops at the first one and says so.\n\nexport const POST_DRAFTS_DEFAULT_LIMIT = 25\n\ntype PostDraftsFailure = { invoiceId: string; number: string; error: string; code: string | null }\n\nexport type PostDraftsResult = {\n posted: number\n failed: PostDraftsFailure[]\n /** Last invoice id handled \u2014 pass back as `afterInvoiceId`. Null when nothing was processed. */\n nextCursor: string | null\n /** Drafts of the run after the cursor that are still to process. */\n remaining: number\n /** Set when the batch stopped early (e.g. `billing.settings.incomplete`). */\n stoppedReason: string | null\n}\n\nconst DRAFTS_AFTER_SQL = `SELECT id, number FROM billing_invoices\n WHERE tenant_id = ? AND organization_id = ? AND bill_run_id = ?\n AND status = 'draft' AND test_run = false AND document_type = 'invoice' AND deleted_at IS NULL\n AND (?::uuid IS NULL OR id > ?::uuid)\n ORDER BY id\n LIMIT ?`\n\nconst DRAFTS_REMAINING_SQL = `SELECT COUNT(*)::int AS remaining FROM billing_invoices\n WHERE tenant_id = ? AND organization_id = ? AND bill_run_id = ?\n AND status = 'draft' AND test_run = false AND document_type = 'invoice' AND deleted_at IS NULL\n AND (?::uuid IS NULL OR id > ?::uuid)`\n\nconst postDraftsCommand: CommandHandler<BillingRunPostDraftsInput, PostDraftsResult> = {\n id: 'billing.runs.post_drafts',\n\n async execute(rawInput, ctx) {\n const parsed = billingRunPostDraftsSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const run = await em.findOne(BillingRun, {\n id: parsed.billRunId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!run) throw new CrudHttpError(404, { error: 'Bill Run not found', code: 'billing.run.not_found' })\n if (runMode(run) !== 'real') {\n throw new CrudHttpError(422, { error: 'Only real Bill Runs have invoices to post', code: 'billing.run.not_real' })\n }\n if (run.status === 'running') {\n throw new CrudHttpError(409, { error: 'The Bill Run is still running', code: 'billing.run.running' })\n }\n\n const scope = [parsed.tenantId, parsed.organizationId, run.id] as const\n const cursor = parsed.afterInvoiceId ?? null\n const limit = parsed.limit ?? POST_DRAFTS_DEFAULT_LIMIT\n const drafts = (await em.execute(DRAFTS_AFTER_SQL, [...scope, cursor, cursor, limit])) as unknown as Array<{\n id: string\n number: string\n }>\n\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const failed: PostDraftsFailure[] = []\n let posted = 0\n let nextCursor: string | null = null\n let stoppedReason: string | null = null\n for (const draft of drafts ?? []) {\n nextCursor = draft.id\n try {\n await commandBus.execute('billing.invoices.post', {\n input: { tenantId: parsed.tenantId, organizationId: parsed.organizationId, invoiceId: draft.id },\n ctx,\n })\n posted += 1\n } catch (err) {\n const body = isCrudHttpError(err) ? (err.body as { error?: string; code?: string }) : null\n const code = body?.code ?? null\n failed.push({\n invoiceId: draft.id,\n number: draft.number,\n error: body?.error ?? (err instanceof Error ? err.message : String(err)),\n code,\n })\n if (code === 'billing.settings.incomplete') {\n // Every other draft would fail the same way \u2014 stop. The returned\n // cursor stays where this call started; drafts posted in this call\n // are no longer drafts, so a retry after fixing the settings picks\n // up exactly the ones still pending.\n stoppedReason = code\n break\n }\n }\n }\n\n const remainingRows = (await em.execute(DRAFTS_REMAINING_SQL, [\n ...scope,\n stoppedReason ? cursor : nextCursor,\n stoppedReason ? cursor : nextCursor,\n ])) as unknown as Array<{ remaining: number }>\n return {\n posted,\n failed,\n nextCursor: stoppedReason ? cursor : nextCursor,\n remaining: Number(remainingRows?.[0]?.remaining ?? 0),\n stoppedReason,\n }\n },\n}\n\nregisterCommand(triggerRunCommand)\nregisterCommand(retryFailedCommand)\nregisterCommand(deleteRunCommand)\nregisterCommand(postDraftsCommand)\n\nexport { triggerRunCommand, retryFailedCommand, deleteRunCommand, postDraftsCommand }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
2
|
+
import { ensureOrganizationScope, ensureTenantScope } from "@open-mercato/shared/lib/commands/scope";
|
|
3
|
+
import { billingSettingsUpdateSchema } from "../data/validators.js";
|
|
4
|
+
import { getBillingSettings, serializeBillingSettings, taxRateColumn } from "../lib/billingSettings.js";
|
|
5
|
+
function getEm(ctx) {
|
|
6
|
+
return ctx.container.resolve("em").fork();
|
|
7
|
+
}
|
|
8
|
+
function money4(value) {
|
|
9
|
+
return (Math.round(value * 1e4) / 1e4).toFixed(4);
|
|
10
|
+
}
|
|
11
|
+
const updateSettingsCommand = {
|
|
12
|
+
id: "billing.settings.update",
|
|
13
|
+
async execute(rawInput, ctx) {
|
|
14
|
+
const parsed = billingSettingsUpdateSchema.parse(rawInput);
|
|
15
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
16
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
17
|
+
const em = getEm(ctx);
|
|
18
|
+
const settings = await getBillingSettings(em, {
|
|
19
|
+
tenantId: parsed.tenantId,
|
|
20
|
+
organizationId: parsed.organizationId
|
|
21
|
+
});
|
|
22
|
+
if (parsed.lowBalanceThresholdDefault !== void 0) {
|
|
23
|
+
settings.lowBalanceThresholdDefault = money4(parsed.lowBalanceThresholdDefault);
|
|
24
|
+
}
|
|
25
|
+
if (parsed.nearLimitBufferDefault !== void 0) {
|
|
26
|
+
settings.nearLimitBufferDefault = money4(parsed.nearLimitBufferDefault);
|
|
27
|
+
}
|
|
28
|
+
if (parsed.statementEnabled !== void 0) settings.statementEnabled = parsed.statementEnabled;
|
|
29
|
+
if (parsed.topupTaxRate !== void 0) settings.topupTaxRate = parsed.topupTaxRate.toFixed(4);
|
|
30
|
+
if (parsed.topupProvider !== void 0) settings.topupProvider = parsed.topupProvider;
|
|
31
|
+
if (parsed.prepaidTaxPoint !== void 0) settings.prepaidTaxPoint = parsed.prepaidTaxPoint;
|
|
32
|
+
if (parsed.numberMasks !== void 0) {
|
|
33
|
+
settings.numberMasks = { ...settings.numberMasks ?? {}, ...stripUndefined(parsed.numberMasks) };
|
|
34
|
+
}
|
|
35
|
+
if (parsed.legalName !== void 0) settings.legalName = parsed.legalName;
|
|
36
|
+
if (parsed.sellerAddress !== void 0) settings.sellerAddress = parsed.sellerAddress;
|
|
37
|
+
if (parsed.taxId !== void 0) settings.taxId = parsed.taxId;
|
|
38
|
+
if (parsed.bankName !== void 0) settings.bankName = parsed.bankName;
|
|
39
|
+
if (parsed.iban !== void 0) settings.iban = parsed.iban;
|
|
40
|
+
if (parsed.bic !== void 0) settings.bic = parsed.bic;
|
|
41
|
+
if (parsed.invoiceFooter !== void 0) settings.invoiceFooter = parsed.invoiceFooter;
|
|
42
|
+
if (parsed.logoAttachmentId !== void 0) settings.logoAttachmentId = parsed.logoAttachmentId;
|
|
43
|
+
if (parsed.defaultPaymentTermsDays !== void 0) settings.defaultPaymentTermsDays = parsed.defaultPaymentTermsDays;
|
|
44
|
+
if (parsed.defaultTaxRate !== void 0) settings.defaultTaxRate = taxRateColumn(parsed.defaultTaxRate);
|
|
45
|
+
settings.updatedAt = /* @__PURE__ */ new Date();
|
|
46
|
+
await em.flush();
|
|
47
|
+
return serializeBillingSettings(settings);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
function stripUndefined(masks) {
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const [key, value] of Object.entries(masks)) if (value !== void 0) out[key] = value;
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
registerCommand(updateSettingsCommand);
|
|
56
|
+
export {
|
|
57
|
+
updateSettingsCommand
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/commands/settings.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'\nimport { billingSettingsUpdateSchema, type BillingSettingsUpdateInput } from '../data/validators'\nimport { getBillingSettings, serializeBillingSettings, taxRateColumn } from '../lib/billingSettings'\n\n/**\n * `billing.settings.update` \u2014 edits the organization's billing settings\n * (Spec 0: prepaid defaults + document number masks; Spec B: seller,\n * payment and document fields). The row always exists (lazily created), so this is a partial\n * update of one row per organization. Masks are validated by the schema.\n */\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction money4(value: number): string {\n return (Math.round(value * 10000) / 10000).toFixed(4)\n}\n\nconst updateSettingsCommand: CommandHandler<\n BillingSettingsUpdateInput,\n ReturnType<typeof serializeBillingSettings>\n> = {\n id: 'billing.settings.update',\n\n async execute(rawInput, ctx) {\n const parsed = billingSettingsUpdateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const settings = await getBillingSettings(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n if (parsed.lowBalanceThresholdDefault !== undefined) {\n settings.lowBalanceThresholdDefault = money4(parsed.lowBalanceThresholdDefault)\n }\n if (parsed.nearLimitBufferDefault !== undefined) {\n settings.nearLimitBufferDefault = money4(parsed.nearLimitBufferDefault)\n }\n if (parsed.statementEnabled !== undefined) settings.statementEnabled = parsed.statementEnabled\n if (parsed.topupTaxRate !== undefined) settings.topupTaxRate = parsed.topupTaxRate.toFixed(4)\n if (parsed.topupProvider !== undefined) settings.topupProvider = parsed.topupProvider\n if (parsed.prepaidTaxPoint !== undefined) settings.prepaidTaxPoint = parsed.prepaidTaxPoint\n if (parsed.numberMasks !== undefined) {\n settings.numberMasks = { ...(settings.numberMasks ?? {}), ...stripUndefined(parsed.numberMasks) }\n }\n // Spec B \u2014 seller / payment / document fields. `null` clears a field.\n if (parsed.legalName !== undefined) settings.legalName = parsed.legalName\n if (parsed.sellerAddress !== undefined) settings.sellerAddress = parsed.sellerAddress\n if (parsed.taxId !== undefined) settings.taxId = parsed.taxId\n if (parsed.bankName !== undefined) settings.bankName = parsed.bankName\n if (parsed.iban !== undefined) settings.iban = parsed.iban\n if (parsed.bic !== undefined) settings.bic = parsed.bic\n if (parsed.invoiceFooter !== undefined) settings.invoiceFooter = parsed.invoiceFooter\n if (parsed.logoAttachmentId !== undefined) settings.logoAttachmentId = parsed.logoAttachmentId\n if (parsed.defaultPaymentTermsDays !== undefined) settings.defaultPaymentTermsDays = parsed.defaultPaymentTermsDays\n if (parsed.defaultTaxRate !== undefined) settings.defaultTaxRate = taxRateColumn(parsed.defaultTaxRate)\n settings.updatedAt = new Date()\n await em.flush()\n return serializeBillingSettings(settings)\n },\n}\n\nfunction stripUndefined(masks: Record<string, string | undefined>): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(masks)) if (value !== undefined) out[key] = value\n return out\n}\n\nregisterCommand(updateSettingsCommand)\n\nexport { updateSettingsCommand }\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,uBAAuB;AAChC,SAAS,yBAAyB,yBAAyB;AAC3D,SAAS,mCAAoE;AAC7E,SAAS,oBAAoB,0BAA0B,qBAAqB;AAS5E,SAAS,MAAM,KAA2C;AACxD,SAAQ,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC7D;AAEA,SAAS,OAAO,OAAuB;AACrC,UAAQ,KAAK,MAAM,QAAQ,GAAK,IAAI,KAAO,QAAQ,CAAC;AACtD;AAEA,MAAM,wBAGF;AAAA,EACF,IAAI;AAAA,EAEJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,4BAA4B,MAAM,QAAQ;AACzD,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAElD,UAAM,KAAK,MAAM,GAAG;AACpB,UAAM,WAAW,MAAM,mBAAmB,IAAI;AAAA,MAC5C,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,IACzB,CAAC;AACD,QAAI,OAAO,+BAA+B,QAAW;AACnD,eAAS,6BAA6B,OAAO,OAAO,0BAA0B;AAAA,IAChF;AACA,QAAI,OAAO,2BAA2B,QAAW;AAC/C,eAAS,yBAAyB,OAAO,OAAO,sBAAsB;AAAA,IACxE;AACA,QAAI,OAAO,qBAAqB,OAAW,UAAS,mBAAmB,OAAO;AAC9E,QAAI,OAAO,iBAAiB,OAAW,UAAS,eAAe,OAAO,aAAa,QAAQ,CAAC;AAC5F,QAAI,OAAO,kBAAkB,OAAW,UAAS,gBAAgB,OAAO;AACxE,QAAI,OAAO,oBAAoB,OAAW,UAAS,kBAAkB,OAAO;AAC5E,QAAI,OAAO,gBAAgB,QAAW;AACpC,eAAS,cAAc,EAAE,GAAI,SAAS,eAAe,CAAC,GAAI,GAAG,eAAe,OAAO,WAAW,EAAE;AAAA,IAClG;AAEA,QAAI,OAAO,cAAc,OAAW,UAAS,YAAY,OAAO;AAChE,QAAI,OAAO,kBAAkB,OAAW,UAAS,gBAAgB,OAAO;AACxE,QAAI,OAAO,UAAU,OAAW,UAAS,QAAQ,OAAO;AACxD,QAAI,OAAO,aAAa,OAAW,UAAS,WAAW,OAAO;AAC9D,QAAI,OAAO,SAAS,OAAW,UAAS,OAAO,OAAO;AACtD,QAAI,OAAO,QAAQ,OAAW,UAAS,MAAM,OAAO;AACpD,QAAI,OAAO,kBAAkB,OAAW,UAAS,gBAAgB,OAAO;AACxE,QAAI,OAAO,qBAAqB,OAAW,UAAS,mBAAmB,OAAO;AAC9E,QAAI,OAAO,4BAA4B,OAAW,UAAS,0BAA0B,OAAO;AAC5F,QAAI,OAAO,mBAAmB,OAAW,UAAS,iBAAiB,cAAc,OAAO,cAAc;AACtG,aAAS,YAAY,oBAAI,KAAK;AAC9B,UAAM,GAAG,MAAM;AACf,WAAO,yBAAyB,QAAQ;AAAA,EAC1C;AACF;AAEA,SAAS,eAAe,OAAmE;AACzF,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,EAAG,KAAI,UAAU,OAAW,KAAI,GAAG,IAAI;AACtF,SAAO;AACT;AAEA,gBAAgB,qBAAqB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "@open-mercato/shared/lib/commands/scope";
|
|
7
7
|
import { emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
|
|
8
8
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
9
|
+
import { resolvePaymentGateway } from "../lib/paymentGateway.js";
|
|
9
10
|
import { BillingAccount, BillingTopup } from "../data/entities.js";
|
|
10
11
|
import { billingEntityIds } from "../data/entityIds.js";
|
|
11
12
|
import {
|
|
@@ -62,7 +63,14 @@ const createTopupCommand = {
|
|
|
62
63
|
};
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
|
-
const
|
|
66
|
+
const gateway = resolvePaymentGateway(ctx.container);
|
|
67
|
+
if (!gateway) {
|
|
68
|
+
throw new CrudHttpError(501, {
|
|
69
|
+
error: "Online top-ups need the payment gateways module, which is not installed",
|
|
70
|
+
code: "billing.topup.gateway_unavailable"
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const providerKey = parsed.providerKey ?? await getTopupProvider(em, { tenantId: parsed.tenantId, organizationId: parsed.organizationId });
|
|
66
74
|
const paymentId = randomUUID();
|
|
67
75
|
const now = /* @__PURE__ */ new Date();
|
|
68
76
|
const topup = em.create(BillingTopup, {
|
|
@@ -84,7 +92,6 @@ const createTopupCommand = {
|
|
|
84
92
|
});
|
|
85
93
|
em.persist(topup);
|
|
86
94
|
await em.flush();
|
|
87
|
-
const gateway = ctx.container.resolve("paymentGatewayService");
|
|
88
95
|
const { transaction, session } = await gateway.createPaymentSession({
|
|
89
96
|
providerKey,
|
|
90
97
|
paymentId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/commands/topups.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport
|
|
5
|
-
"mappings": "AAAA,SAAS,kBAAkB;AAG3B,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { resolvePaymentGateway } from '../lib/paymentGateway'\nimport { BillingAccount, BillingTopup } from '../data/entities'\nimport { billingEntityIds } from '../data/entityIds'\nimport {\n billingTopupCreateSchema,\n type BillingTopupCreateInput,\n} from '../data/validators'\nimport { formatMoney } from '../lib/money'\nimport { getTopupProvider } from '../lib/prepaidConfig'\n\n/**\n * Top-up initiation (SPEC-002 P2).\n *\n * Registers a first-class `BillingTopup` (status=pending), then opens a\n * payment session via `core/payment_gateways`. The balance is NOT credited\n * here \u2014 that happens on the `payment_gateways.payment.captured` event (see\n * subscribers/prepaid-topup-captured.ts), so the money trail is always\n * explainable: a top-up only credits the balance once the gateway confirms\n * capture.\n */\n\nconst topupIndexer = { entityType: billingEntityIds.topup } as const\nconst topupEvents = { module: 'billing', entity: 'topup', persistent: true } as const\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction getDataEngine(ctx: CommandRuntimeContext): DataEngine {\n return ctx.container.resolve('dataEngine') as DataEngine\n}\n\nexport type CreateTopupResult = {\n topupId: string\n paymentId: string\n status: string\n transactionId: string | null\n redirectUrl: string | null\n clientSecret: string | null\n}\n\nconst createTopupCommand: CommandHandler<BillingTopupCreateInput, CreateTopupResult> = {\n id: 'billing.topups.create',\n\n async execute(rawInput, ctx) {\n const parsed = billingTopupCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const account = await em.findOne(BillingAccount, {\n id: parsed.billAccountId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!account) {\n throw new CrudHttpError(404, { error: 'Billing account not found' })\n }\n if (account.billingMode !== 'prepaid') {\n throw new CrudHttpError(409, {\n error: 'Top-ups are only available on prepaid accounts',\n code: 'billing.account.not_prepaid',\n })\n }\n\n // Idempotency on the initiating request: a retried POST with the same\n // sourceRef returns the prior (still-pending) top-up + its session links\n // rather than opening a second session.\n if (parsed.sourceRef) {\n const existing = await em.findOne(BillingTopup, {\n tenantId: parsed.tenantId,\n billAccountId: account.id,\n sourceRef: parsed.sourceRef,\n deletedAt: null,\n })\n if (existing) {\n const meta = (existing.metadata as Record<string, unknown> | null) ?? {}\n return {\n topupId: existing.id,\n paymentId: existing.paymentId,\n status: existing.status,\n transactionId: existing.gatewayTransactionId ?? null,\n redirectUrl: (meta.redirect_url as string | null) ?? null,\n clientSecret: (meta.client_secret as string | null) ?? null,\n }\n }\n }\n\n // E-D1: the gateway is optional. Check before persisting anything so an\n // install without `payment_gateways` never leaves a dangling pending top-up.\n const gateway = resolvePaymentGateway(ctx.container as never)\n if (!gateway) {\n throw new CrudHttpError(501, {\n error: 'Online top-ups need the payment gateways module, which is not installed',\n code: 'billing.topup.gateway_unavailable',\n })\n }\n\n const providerKey = parsed.providerKey ?? (await getTopupProvider(em, { tenantId: parsed.tenantId, organizationId: parsed.organizationId }))\n const paymentId = randomUUID()\n const now = new Date()\n const topup = em.create(BillingTopup, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: account.id,\n status: 'pending',\n amount: formatMoney(parsed.amount),\n currencyCode: account.currencyCode,\n providerKey,\n paymentId,\n sourceRef: parsed.sourceRef ?? null,\n metadata: {\n success_url: parsed.successUrl ?? null,\n cancel_url: parsed.cancelUrl ?? null,\n },\n createdAt: now,\n updatedAt: now,\n })\n em.persist(topup)\n await em.flush()\n\n // Open the gateway session. captureMethod 'automatic' so a successful\n // checkout fires `payment.captured` directly (our capture subscriber\n // credits the balance + issues the receipt).\n const { transaction, session } = await gateway.createPaymentSession({\n providerKey,\n paymentId,\n amount: parsed.amount,\n currencyCode: account.currencyCode,\n captureMethod: 'automatic',\n successUrl: parsed.successUrl,\n cancelUrl: parsed.cancelUrl,\n metadata: {\n kind: 'prepaid_topup',\n billAccountId: account.id,\n topupId: topup.id,\n },\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n })\n\n topup.gatewayTransactionId = transaction.id\n topup.metadata = {\n ...(topup.metadata as Record<string, unknown>),\n redirect_url: session.redirectUrl ?? null,\n client_secret: session.clientSecret ?? null,\n session_id: session.sessionId ?? null,\n }\n topup.updatedAt = new Date()\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'created',\n entity: topup,\n identifiers: {\n id: topup.id,\n tenantId: topup.tenantId,\n organizationId: topup.organizationId,\n },\n indexer: topupIndexer,\n events: topupEvents,\n })\n\n return {\n topupId: topup.id,\n paymentId,\n status: topup.status,\n transactionId: transaction.id,\n redirectUrl: session.redirectUrl ?? null,\n clientSecret: session.clientSecret ?? null,\n }\n },\n}\n\nregisterCommand(createTopupCommand)\n\nexport { createTopupCommand }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -55,9 +55,9 @@ const adjustTransactionCommand = {
|
|
|
55
55
|
});
|
|
56
56
|
const threshold = resolveLowBalanceThreshold(
|
|
57
57
|
balanceRow?.lowBalanceThreshold ?? null,
|
|
58
|
-
await getLowBalanceThresholdDefault(em)
|
|
58
|
+
await getLowBalanceThresholdDefault(em, { tenantId: account.tenantId, organizationId: account.organizationId })
|
|
59
59
|
);
|
|
60
|
-
const nearLimitBuffer = await getNearLimitBufferDefault(em);
|
|
60
|
+
const nearLimitBuffer = await getNearLimitBufferDefault(em, { tenantId: account.tenantId, organizationId: account.organizationId });
|
|
61
61
|
const userId = ctx.auth?.sub ?? null;
|
|
62
62
|
const outcome = await em.transactional(
|
|
63
63
|
async (tem) => applyBalanceMovement(tem, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/commands/transactions.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { BillingAccount, BillingAccountBalance } from '../data/entities'\nimport { billingEntityIds } from '../data/entityIds'\nimport {\n billingTransactionAdjustSchema,\n type BillingTransactionAdjustInput,\n type BalanceStatus,\n} from '../data/validators'\nimport { applyBalanceMovement } from '../lib/balanceLedger'\nimport { buildBalanceCrossingEvents } from '../lib/balanceEvents'\nimport { computeBalanceStatus, resolveLowBalanceThreshold } from '../lib/balanceStatus'\nimport { formatMoney } from '../lib/money'\nimport {\n getLowBalanceThresholdDefault,\n getNearLimitBufferDefault,\n} from '../lib/prepaidConfig'\nimport { emitBillingEvent, type BillingEventId } from '../events'\n\n/**\n * Manual balance adjustment (SPEC-002 P5).\n *\n * Operator-driven credit/debit (`type='adjustment'`) audited via `user_id`.\n * Non-fiscal by default \u2014 an adjustment moves already-taxed balance, so no\n * receipt is issued (Open Question #3; can be revisited per jurisdiction).\n * Goes through the same atomic ledger primitive as every other movement, so\n * the reconciliation invariant still holds.\n */\n\nconst transactionIndexer = { entityType: billingEntityIds.transaction } as const\nconst transactionEvents = { module: 'billing', entity: 'transaction', persistent: true } as const\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction getDataEngine(ctx: CommandRuntimeContext): DataEngine {\n return ctx.container.resolve('dataEngine') as DataEngine\n}\n\nexport type AdjustResult = {\n transactionId: string\n balance: string\n balanceStatus: BalanceStatus\n deduplicated: boolean\n}\n\nconst adjustTransactionCommand: CommandHandler<BillingTransactionAdjustInput, AdjustResult> = {\n id: 'billing.transactions.adjust',\n\n async execute(rawInput, ctx) {\n const parsed = billingTransactionAdjustSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const account = await em.findOne(BillingAccount, {\n id: parsed.billAccountId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!account) {\n throw new CrudHttpError(404, { error: 'Billing account not found' })\n }\n if (account.billingMode !== 'prepaid') {\n throw new CrudHttpError(409, {\n error: 'Balance adjustments are only available on prepaid accounts',\n code: 'billing.account.not_prepaid',\n })\n }\n\n const balanceRow = await em.findOne(BillingAccountBalance, {\n tenantId: parsed.tenantId,\n billAccountId: account.id,\n })\n const threshold = resolveLowBalanceThreshold(\n balanceRow?.lowBalanceThreshold ?? null,\n await getLowBalanceThresholdDefault(em),\n )\n const nearLimitBuffer = await getNearLimitBufferDefault(em)\n const userId = (ctx.auth?.sub as string | undefined) ?? null\n\n const outcome = await em.transactional(async (tem) =>\n applyBalanceMovement(tem as EntityManager, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: account.id,\n currencyCode: account.currencyCode,\n type: 'adjustment',\n amount: formatMoney(parsed.amount),\n description: parsed.description,\n sourceRef: parsed.sourceRef ?? null,\n userId,\n metadata: { manual: true },\n }),\n )\n\n // Index the transaction + emit threshold-crossing events after commit.\n const created = outcome.transaction\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'created',\n entity: created,\n identifiers: {\n id: created.id,\n tenantId: created.tenantId,\n organizationId: created.organizationId,\n },\n indexer: transactionIndexer,\n events: transactionEvents,\n })\n\n if (!outcome.deduplicated) {\n const events = buildBalanceCrossingEvents({\n scope: {\n billAccountId: account.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n currencyCode: account.currencyCode,\n },\n balanceBefore: outcome.balanceBefore,\n balanceAfter: outcome.balanceAfter,\n threshold,\n creditLimit: account.creditLimit ?? '0',\n nearLimitBuffer,\n })\n for (const ev of events) {\n await emitBillingEvent(ev.id as BillingEventId, ev.payload, { persistent: true })\n }\n }\n\n return {\n transactionId: created.id,\n balance: outcome.balanceAfter,\n balanceStatus: computeBalanceStatus(outcome.balanceAfter, threshold),\n deduplicated: outcome.deduplicated,\n }\n },\n}\n\nregisterCommand(adjustTransactionCommand)\n\nexport { adjustTransactionCommand }\n"],
|
|
5
|
-
"mappings": "AAEA,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;AAE9B,SAAS,gBAAgB,6BAA6B;AACtD,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,OAGK;AACP,SAAS,4BAA4B;AACrC,SAAS,kCAAkC;AAC3C,SAAS,sBAAsB,kCAAkC;AACjE,SAAS,mBAAmB;AAC5B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,wBAA6C;AAYtD,MAAM,qBAAqB,EAAE,YAAY,iBAAiB,YAAY;AACtE,MAAM,oBAAoB,EAAE,QAAQ,WAAW,QAAQ,eAAe,YAAY,KAAK;AAEvF,SAAS,MAAM,KAA2C;AACxD,SAAQ,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC7D;AAEA,SAAS,cAAc,KAAwC;AAC7D,SAAO,IAAI,UAAU,QAAQ,YAAY;AAC3C;AASA,MAAM,2BAAwF;AAAA,EAC5F,IAAI;AAAA,EAEJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,+BAA+B,MAAM,QAAQ;AAC5D,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAElD,UAAM,KAAK,MAAM,GAAG;AACpB,UAAM,UAAU,MAAM,GAAG,QAAQ,gBAAgB;AAAA,MAC/C,IAAI,OAAO;AAAA,MACX,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,4BAA4B,CAAC;AAAA,IACrE;AACA,QAAI,QAAQ,gBAAgB,WAAW;AACrC,YAAM,IAAI,cAAc,KAAK;AAAA,QAC3B,OAAO;AAAA,QACP,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,UAAM,aAAa,MAAM,GAAG,QAAQ,uBAAuB;AAAA,MACzD,UAAU,OAAO;AAAA,MACjB,eAAe,QAAQ;AAAA,IACzB,CAAC;AACD,UAAM,YAAY;AAAA,MAChB,YAAY,uBAAuB;AAAA,MACnC,MAAM,8BAA8B,EAAE;AAAA,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { BillingAccount, BillingAccountBalance } from '../data/entities'\nimport { billingEntityIds } from '../data/entityIds'\nimport {\n billingTransactionAdjustSchema,\n type BillingTransactionAdjustInput,\n type BalanceStatus,\n} from '../data/validators'\nimport { applyBalanceMovement } from '../lib/balanceLedger'\nimport { buildBalanceCrossingEvents } from '../lib/balanceEvents'\nimport { computeBalanceStatus, resolveLowBalanceThreshold } from '../lib/balanceStatus'\nimport { formatMoney } from '../lib/money'\nimport {\n getLowBalanceThresholdDefault,\n getNearLimitBufferDefault,\n} from '../lib/prepaidConfig'\nimport { emitBillingEvent, type BillingEventId } from '../events'\n\n/**\n * Manual balance adjustment (SPEC-002 P5).\n *\n * Operator-driven credit/debit (`type='adjustment'`) audited via `user_id`.\n * Non-fiscal by default \u2014 an adjustment moves already-taxed balance, so no\n * receipt is issued (Open Question #3; can be revisited per jurisdiction).\n * Goes through the same atomic ledger primitive as every other movement, so\n * the reconciliation invariant still holds.\n */\n\nconst transactionIndexer = { entityType: billingEntityIds.transaction } as const\nconst transactionEvents = { module: 'billing', entity: 'transaction', persistent: true } as const\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction getDataEngine(ctx: CommandRuntimeContext): DataEngine {\n return ctx.container.resolve('dataEngine') as DataEngine\n}\n\nexport type AdjustResult = {\n transactionId: string\n balance: string\n balanceStatus: BalanceStatus\n deduplicated: boolean\n}\n\nconst adjustTransactionCommand: CommandHandler<BillingTransactionAdjustInput, AdjustResult> = {\n id: 'billing.transactions.adjust',\n\n async execute(rawInput, ctx) {\n const parsed = billingTransactionAdjustSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const account = await em.findOne(BillingAccount, {\n id: parsed.billAccountId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!account) {\n throw new CrudHttpError(404, { error: 'Billing account not found' })\n }\n if (account.billingMode !== 'prepaid') {\n throw new CrudHttpError(409, {\n error: 'Balance adjustments are only available on prepaid accounts',\n code: 'billing.account.not_prepaid',\n })\n }\n\n const balanceRow = await em.findOne(BillingAccountBalance, {\n tenantId: parsed.tenantId,\n billAccountId: account.id,\n })\n const threshold = resolveLowBalanceThreshold(\n balanceRow?.lowBalanceThreshold ?? null,\n await getLowBalanceThresholdDefault(em, { tenantId: account.tenantId, organizationId: account.organizationId }),\n )\n const nearLimitBuffer = await getNearLimitBufferDefault(em, { tenantId: account.tenantId, organizationId: account.organizationId })\n const userId = (ctx.auth?.sub as string | undefined) ?? null\n\n const outcome = await em.transactional(async (tem) =>\n applyBalanceMovement(tem as EntityManager, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: account.id,\n currencyCode: account.currencyCode,\n type: 'adjustment',\n amount: formatMoney(parsed.amount),\n description: parsed.description,\n sourceRef: parsed.sourceRef ?? null,\n userId,\n metadata: { manual: true },\n }),\n )\n\n // Index the transaction + emit threshold-crossing events after commit.\n const created = outcome.transaction\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'created',\n entity: created,\n identifiers: {\n id: created.id,\n tenantId: created.tenantId,\n organizationId: created.organizationId,\n },\n indexer: transactionIndexer,\n events: transactionEvents,\n })\n\n if (!outcome.deduplicated) {\n const events = buildBalanceCrossingEvents({\n scope: {\n billAccountId: account.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n currencyCode: account.currencyCode,\n },\n balanceBefore: outcome.balanceBefore,\n balanceAfter: outcome.balanceAfter,\n threshold,\n creditLimit: account.creditLimit ?? '0',\n nearLimitBuffer,\n })\n for (const ev of events) {\n await emitBillingEvent(ev.id as BillingEventId, ev.payload, { persistent: true })\n }\n }\n\n return {\n transactionId: created.id,\n balance: outcome.balanceAfter,\n balanceStatus: computeBalanceStatus(outcome.balanceAfter, threshold),\n deduplicated: outcome.deduplicated,\n }\n },\n}\n\nregisterCommand(adjustTransactionCommand)\n\nexport { adjustTransactionCommand }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|