@dainamite/billing 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/dist/modules/billing/acl.js +13 -1
- package/dist/modules/billing/acl.js.map +2 -2
- package/dist/modules/billing/api/accounts/[id]/summary/route.js +52 -0
- package/dist/modules/billing/api/accounts/[id]/summary/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +47 -4
- package/dist/modules/billing/api/accounts/route.js.map +2 -2
- package/dist/modules/billing/api/export/account/[id]/route.js +11 -7
- package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/[id]/memos/route.js +54 -0
- package/dist/modules/billing/api/invoices/[id]/memos/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/pdf/route.js +70 -0
- package/dist/modules/billing/api/invoices/[id]/pdf/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/route.js +44 -24
- package/dist/modules/billing/api/invoices/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/[id]/send/route.js +107 -0
- package/dist/modules/billing/api/invoices/[id]/send/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/add-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/add-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/delete-draft/route.js +33 -0
- package/dist/modules/billing/api/invoices/delete-draft/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/edit-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/post/route.js +2 -2
- package/dist/modules/billing/api/invoices/post/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/remove-line/route.js +2 -2
- package/dist/modules/billing/api/invoices/remove-line/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/route.js +45 -46
- package/dist/modules/billing/api/invoices/route.js.map +2 -2
- package/dist/modules/billing/api/items/bulk/route.js +1 -1
- package/dist/modules/billing/api/items/bulk/route.js.map +2 -2
- package/dist/modules/billing/api/items/change-price/route.js +44 -0
- package/dist/modules/billing/api/items/change-price/route.js.map +7 -0
- package/dist/modules/billing/api/items/route.js +19 -7
- package/dist/modules/billing/api/items/route.js.map +2 -2
- package/dist/modules/billing/api/payment-imports/[id]/route.js +49 -0
- package/dist/modules/billing/api/payment-imports/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/payment-imports/route.js +57 -0
- package/dist/modules/billing/api/payment-imports/route.js.map +7 -0
- package/dist/modules/billing/api/payments/[id]/route.js +60 -0
- package/dist/modules/billing/api/payments/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/payments/allocate/route.js +35 -0
- package/dist/modules/billing/api/payments/allocate/route.js.map +7 -0
- package/dist/modules/billing/api/payments/assign/route.js +36 -0
- package/dist/modules/billing/api/payments/assign/route.js.map +7 -0
- package/dist/modules/billing/api/payments/convert-to-topup/route.js +35 -0
- package/dist/modules/billing/api/payments/convert-to-topup/route.js.map +7 -0
- package/dist/modules/billing/api/payments/ignore/route.js +35 -0
- package/dist/modules/billing/api/payments/ignore/route.js.map +7 -0
- package/dist/modules/billing/api/payments/import/route.js +63 -0
- package/dist/modules/billing/api/payments/import/route.js.map +7 -0
- package/dist/modules/billing/api/payments/restore/route.js +35 -0
- package/dist/modules/billing/api/payments/restore/route.js.map +7 -0
- package/dist/modules/billing/api/payments/route.js +114 -0
- package/dist/modules/billing/api/payments/route.js.map +7 -0
- package/dist/modules/billing/api/payments/unallocate/route.js +35 -0
- package/dist/modules/billing/api/payments/unallocate/route.js.map +7 -0
- package/dist/modules/billing/api/payments/unassign/route.js +35 -0
- package/dist/modules/billing/api/payments/unassign/route.js.map +7 -0
- package/dist/modules/billing/api/run-outcomes/route.js +43 -6
- package/dist/modules/billing/api/run-outcomes/route.js.map +2 -2
- package/dist/modules/billing/api/runs/[id]/post-drafts/route.js +61 -0
- package/dist/modules/billing/api/runs/[id]/post-drafts/route.js.map +7 -0
- package/dist/modules/billing/api/runs/[id]/route.js +53 -0
- package/dist/modules/billing/api/runs/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js +1 -1
- package/dist/modules/billing/api/runs/retry-failed/route.js.map +2 -2
- package/dist/modules/billing/api/runs/route.js +9 -3
- package/dist/modules/billing/api/runs/route.js.map +2 -2
- package/dist/modules/billing/api/settings/number-preview/route.js +42 -0
- package/dist/modules/billing/api/settings/number-preview/route.js.map +7 -0
- package/dist/modules/billing/api/settings/route.js +92 -0
- package/dist/modules/billing/api/settings/route.js.map +7 -0
- package/dist/modules/billing/api/statements/route.js +3 -0
- package/dist/modules/billing/api/statements/route.js.map +2 -2
- package/dist/modules/billing/api/test-invoices/wipe/route.js +2 -2
- package/dist/modules/billing/api/test-invoices/wipe/route.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +121 -232
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/create/page.js +6 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/page.js +63 -4
- package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js +297 -65
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +3 -3
- package/dist/modules/billing/backend/billing/invoices/page.js +6 -172
- package/dist/modules/billing/backend/billing/invoices/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/[id]/page.js +51 -7
- package/dist/modules/billing/backend/billing/items/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/create/page.js +9 -6
- package/dist/modules/billing/backend/billing/items/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/items/page.js +8 -2
- package/dist/modules/billing/backend/billing/items/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/payments/[id]/page.js +297 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js +74 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/page.js +58 -0
- package/dist/modules/billing/backend/billing/payments/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/payments/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/payments/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js +49 -14
- package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/runs/page.js +58 -7
- package/dist/modules/billing/backend/billing/runs/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/[id]/page.js +61 -6
- package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/page.js +1 -1
- package/dist/modules/billing/backend/billing/statements/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/topups/page.js +1 -1
- package/dist/modules/billing/backend/billing/topups/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/transactions/page.js +1 -1
- package/dist/modules/billing/backend/billing/transactions/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/usage/create/page.js +3 -2
- package/dist/modules/billing/backend/billing/usage/create/page.js.map +2 -2
- package/dist/modules/billing/backend/config/billing/page.js +472 -0
- package/dist/modules/billing/backend/config/billing/page.js.map +7 -0
- package/dist/modules/billing/backend/config/billing/page.meta.js +19 -0
- package/dist/modules/billing/backend/config/billing/page.meta.js.map +7 -0
- package/dist/modules/billing/cli.js +22 -1
- package/dist/modules/billing/cli.js.map +2 -2
- package/dist/modules/billing/commands/accounts.js +6 -0
- package/dist/modules/billing/commands/accounts.js.map +2 -2
- package/dist/modules/billing/commands/index.js +3 -0
- package/dist/modules/billing/commands/index.js.map +2 -2
- package/dist/modules/billing/commands/invoices.js +269 -167
- package/dist/modules/billing/commands/invoices.js.map +2 -2
- package/dist/modules/billing/commands/items.js +108 -0
- package/dist/modules/billing/commands/items.js.map +2 -2
- package/dist/modules/billing/commands/memos.js +179 -0
- package/dist/modules/billing/commands/memos.js.map +7 -0
- package/dist/modules/billing/commands/payments.js +379 -0
- package/dist/modules/billing/commands/payments.js.map +7 -0
- package/dist/modules/billing/commands/runs.js +151 -4
- package/dist/modules/billing/commands/runs.js.map +2 -2
- package/dist/modules/billing/commands/settings.js +59 -0
- package/dist/modules/billing/commands/settings.js.map +7 -0
- package/dist/modules/billing/commands/topups.js +9 -2
- package/dist/modules/billing/commands/topups.js.map +2 -2
- package/dist/modules/billing/commands/transactions.js +2 -2
- package/dist/modules/billing/commands/transactions.js.map +2 -2
- package/dist/modules/billing/components/AccountForm.js +149 -42
- package/dist/modules/billing/components/AccountForm.js.map +2 -2
- package/dist/modules/billing/components/AccountInvoicesSection.js +64 -0
- package/dist/modules/billing/components/AccountInvoicesSection.js.map +7 -0
- package/dist/modules/billing/components/AccountPaymentsSection.js +43 -0
- package/dist/modules/billing/components/AccountPaymentsSection.js.map +7 -0
- package/dist/modules/billing/components/AccountPicker.js +56 -0
- package/dist/modules/billing/components/AccountPicker.js.map +7 -0
- package/dist/modules/billing/components/AllocatePaymentDialog.js +134 -0
- package/dist/modules/billing/components/AllocatePaymentDialog.js.map +7 -0
- package/dist/modules/billing/components/BillingDataTable.js +21 -0
- package/dist/modules/billing/components/BillingDataTable.js.map +7 -0
- package/dist/modules/billing/components/BillingSettingsLink.js +19 -0
- package/dist/modules/billing/components/BillingSettingsLink.js.map +7 -0
- package/dist/modules/billing/components/ChangeItemPriceDialog.js +158 -0
- package/dist/modules/billing/components/ChangeItemPriceDialog.js.map +7 -0
- package/dist/modules/billing/components/CreateMemoDialog.js +200 -0
- package/dist/modules/billing/components/CreateMemoDialog.js.map +7 -0
- package/dist/modules/billing/components/CustomerLabel.js +33 -0
- package/dist/modules/billing/components/CustomerLabel.js.map +7 -0
- package/dist/modules/billing/components/InvoicesTable.js +270 -0
- package/dist/modules/billing/components/InvoicesTable.js.map +7 -0
- package/dist/modules/billing/components/ItemForm.js +176 -44
- package/dist/modules/billing/components/ItemForm.js.map +2 -2
- package/dist/modules/billing/components/ItemPriceField.js +46 -0
- package/dist/modules/billing/components/ItemPriceField.js.map +7 -0
- package/dist/modules/billing/components/LineFormDialog.js +25 -2
- package/dist/modules/billing/components/LineFormDialog.js.map +2 -2
- package/dist/modules/billing/components/ManualPaymentDialog.js +122 -0
- package/dist/modules/billing/components/ManualPaymentDialog.js.map +7 -0
- package/dist/modules/billing/components/MoneyInput.js +14 -0
- package/dist/modules/billing/components/MoneyInput.js.map +7 -0
- package/dist/modules/billing/components/PaymentUploadDialog.js +115 -0
- package/dist/modules/billing/components/PaymentUploadDialog.js.map +7 -0
- package/dist/modules/billing/components/PaymentsTable.js +171 -0
- package/dist/modules/billing/components/PaymentsTable.js.map +7 -0
- package/dist/modules/billing/components/PostRunDraftsButton.js +105 -0
- package/dist/modules/billing/components/PostRunDraftsButton.js.map +7 -0
- package/dist/modules/billing/components/PrepaidPanel.js +14 -4
- package/dist/modules/billing/components/PrepaidPanel.js.map +2 -2
- package/dist/modules/billing/components/RunCyclesSection.js +209 -0
- package/dist/modules/billing/components/RunCyclesSection.js.map +7 -0
- package/dist/modules/billing/components/TestRunActions.js +127 -0
- package/dist/modules/billing/components/TestRunActions.js.map +7 -0
- package/dist/modules/billing/components/account/AccountItemsTab.js +157 -0
- package/dist/modules/billing/components/account/AccountItemsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountRunBillingDialog.js +122 -0
- package/dist/modules/billing/components/account/AccountRunBillingDialog.js.map +7 -0
- package/dist/modules/billing/components/account/AccountRunsTab.js +152 -0
- package/dist/modules/billing/components/account/AccountRunsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountStatementsTab.js +98 -0
- package/dist/modules/billing/components/account/AccountStatementsTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountSummaryCards.js +125 -0
- package/dist/modules/billing/components/account/AccountSummaryCards.js.map +7 -0
- package/dist/modules/billing/components/account/AccountUsageTab.js +91 -0
- package/dist/modules/billing/components/account/AccountUsageTab.js.map +7 -0
- package/dist/modules/billing/components/account/AccountView.js +73 -0
- package/dist/modules/billing/components/account/AccountView.js.map +7 -0
- package/dist/modules/billing/components/account/accountRow.js +39 -0
- package/dist/modules/billing/components/account/accountRow.js.map +7 -0
- package/dist/modules/billing/components/customerNames.js +42 -0
- package/dist/modules/billing/components/customerNames.js.map +7 -0
- package/dist/modules/billing/data/entities.js +560 -4
- package/dist/modules/billing/data/entities.js.map +2 -2
- package/dist/modules/billing/data/entityIds.js +10 -1
- package/dist/modules/billing/data/entityIds.js.map +2 -2
- package/dist/modules/billing/data/validators.js +269 -15
- package/dist/modules/billing/data/validators.js.map +2 -2
- package/dist/modules/billing/events.js +50 -0
- package/dist/modules/billing/events.js.map +2 -2
- package/dist/modules/billing/i18n/de.json +880 -0
- package/dist/modules/billing/i18n/en.json +457 -21
- package/dist/modules/billing/i18n/es.json +880 -0
- package/dist/modules/billing/i18n/pl.json +458 -22
- package/dist/modules/billing/index.js +6 -2
- package/dist/modules/billing/index.js.map +2 -2
- package/dist/modules/billing/lib/accountFinancials.js +58 -0
- package/dist/modules/billing/lib/accountFinancials.js.map +7 -0
- package/dist/modules/billing/lib/accountLock.js +14 -0
- package/dist/modules/billing/lib/accountLock.js.map +7 -0
- package/dist/modules/billing/lib/accountStatus.js +9 -7
- package/dist/modules/billing/lib/accountStatus.js.map +2 -2
- package/dist/modules/billing/lib/accountSummary.js +106 -0
- package/dist/modules/billing/lib/accountSummary.js.map +7 -0
- package/dist/modules/billing/lib/allocation.js +356 -0
- package/dist/modules/billing/lib/allocation.js.map +7 -0
- package/dist/modules/billing/lib/allocationEvents.js +22 -0
- package/dist/modules/billing/lib/allocationEvents.js.map +7 -0
- package/dist/modules/billing/lib/billPeriod.js +15 -1
- package/dist/modules/billing/lib/billPeriod.js.map +2 -2
- package/dist/modules/billing/lib/billRunEngine.js +237 -102
- package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
- package/dist/modules/billing/lib/billingSettings.js +116 -0
- package/dist/modules/billing/lib/billingSettings.js.map +7 -0
- package/dist/modules/billing/lib/commandRoute.js +81 -0
- package/dist/modules/billing/lib/commandRoute.js.map +7 -0
- package/dist/modules/billing/lib/customerLookup.js +97 -0
- package/dist/modules/billing/lib/customerLookup.js.map +7 -0
- package/dist/modules/billing/lib/dateOnly.js +13 -0
- package/dist/modules/billing/lib/dateOnly.js.map +7 -0
- package/dist/modules/billing/lib/documentParties.js +16 -0
- package/dist/modules/billing/lib/documentParties.js.map +7 -0
- package/dist/modules/billing/lib/documentTypes.js +27 -0
- package/dist/modules/billing/lib/documentTypes.js.map +7 -0
- package/dist/modules/billing/lib/dueDate.js +18 -0
- package/dist/modules/billing/lib/dueDate.js.map +7 -0
- package/dist/modules/billing/lib/iban.js +29 -0
- package/dist/modules/billing/lib/iban.js.map +7 -0
- package/dist/modules/billing/lib/invoiceDelivery.js +133 -0
- package/dist/modules/billing/lib/invoiceDelivery.js.map +7 -0
- package/dist/modules/billing/lib/invoiceLanguages.js +28 -0
- package/dist/modules/billing/lib/invoiceLanguages.js.map +7 -0
- package/dist/modules/billing/lib/invoiceWriter.js +107 -99
- package/dist/modules/billing/lib/invoiceWriter.js.map +2 -2
- package/dist/modules/billing/lib/itemBillingState.js +27 -0
- package/dist/modules/billing/lib/itemBillingState.js.map +7 -0
- package/dist/modules/billing/lib/itemPriceChange.js +18 -0
- package/dist/modules/billing/lib/itemPriceChange.js.map +7 -0
- package/dist/modules/billing/lib/itemSelector.js.map +2 -2
- package/dist/modules/billing/lib/lineAmounts.js +32 -0
- package/dist/modules/billing/lib/lineAmounts.js.map +7 -0
- package/dist/modules/billing/lib/memoGuards.js +25 -0
- package/dist/modules/billing/lib/memoGuards.js.map +7 -0
- package/dist/modules/billing/lib/navigation.js +48 -0
- package/dist/modules/billing/lib/navigation.js.map +7 -0
- package/dist/modules/billing/lib/numbering.js +120 -0
- package/dist/modules/billing/lib/numbering.js.map +7 -0
- package/dist/modules/billing/lib/overdue.js +36 -0
- package/dist/modules/billing/lib/overdue.js.map +7 -0
- package/dist/modules/billing/lib/paymentCsv.js +125 -0
- package/dist/modules/billing/lib/paymentCsv.js.map +7 -0
- package/dist/modules/billing/lib/paymentGateway.js +15 -0
- package/dist/modules/billing/lib/paymentGateway.js.map +7 -0
- package/dist/modules/billing/lib/paymentImport.js +164 -0
- package/dist/modules/billing/lib/paymentImport.js.map +7 -0
- package/dist/modules/billing/lib/paymentMatcher.js +36 -0
- package/dist/modules/billing/lib/paymentMatcher.js.map +7 -0
- package/dist/modules/billing/lib/pdf/attachmentStore.js +46 -0
- package/dist/modules/billing/lib/pdf/attachmentStore.js.map +7 -0
- package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js +7 -0
- package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoiceModel.js +194 -0
- package/dist/modules/billing/lib/pdf/invoiceModel.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoicePdf.js +165 -0
- package/dist/modules/billing/lib/pdf/invoicePdf.js.map +7 -0
- package/dist/modules/billing/lib/pdf/invoiceTemplate.js +276 -0
- package/dist/modules/billing/lib/pdf/invoiceTemplate.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConfig.js +17 -25
- package/dist/modules/billing/lib/prepaidConfig.js.map +2 -2
- package/dist/modules/billing/lib/prepaidConsume.js +26 -7
- package/dist/modules/billing/lib/prepaidConsume.js.map +2 -2
- package/dist/modules/billing/lib/prepaidPeriodClose.js +277 -158
- package/dist/modules/billing/lib/prepaidPeriodClose.js.map +2 -2
- package/dist/modules/billing/lib/prepaidTaxPoint.js +37 -0
- package/dist/modules/billing/lib/prepaidTaxPoint.js.map +7 -0
- package/dist/modules/billing/lib/seeds/demo.js +51 -3
- package/dist/modules/billing/lib/seeds/demo.js.map +2 -2
- package/dist/modules/billing/lib/seeds/paymentsSample.js +93 -0
- package/dist/modules/billing/lib/seeds/paymentsSample.js.map +7 -0
- package/dist/modules/billing/lib/seeds/sellerSettings.js +41 -0
- package/dist/modules/billing/lib/seeds/sellerSettings.js.map +7 -0
- package/dist/modules/billing/lib/servicePeriods.js +121 -0
- package/dist/modules/billing/lib/servicePeriods.js.map +7 -0
- package/dist/modules/billing/lib/testRunCleanup.js +24 -0
- package/dist/modules/billing/lib/testRunCleanup.js.map +7 -0
- package/dist/modules/billing/lib/topupCapture.js +2 -2
- package/dist/modules/billing/lib/topupCapture.js.map +2 -2
- package/dist/modules/billing/lib/topupReceipt.js +58 -79
- package/dist/modules/billing/lib/topupReceipt.js.map +2 -2
- package/dist/modules/billing/lib/usageRater.js.map +1 -1
- package/dist/modules/billing/lib/usageRunner.js +14 -6
- package/dist/modules/billing/lib/usageRunner.js.map +2 -2
- package/dist/modules/billing/lib/vat.js +39 -0
- package/dist/modules/billing/lib/vat.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js +15 -0
- package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js +25 -0
- package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js +115 -0
- package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js +47 -0
- package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js +89 -0
- package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js +17 -0
- package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js +27 -0
- package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js +29 -0
- package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js.map +7 -0
- package/dist/modules/billing/setup.js +7 -184
- package/dist/modules/billing/setup.js.map +2 -2
- package/dist/modules/billing/subscribers/credit-memo-auto-send.js +16 -0
- package/dist/modules/billing/subscribers/credit-memo-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/debit-memo-apply-credit.js +16 -0
- package/dist/modules/billing/subscribers/debit-memo-apply-credit.js.map +7 -0
- package/dist/modules/billing/subscribers/debit-memo-auto-send.js +16 -0
- package/dist/modules/billing/subscribers/debit-memo-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/invoice-apply-credit.js +34 -0
- package/dist/modules/billing/subscribers/invoice-apply-credit.js.map +7 -0
- package/dist/modules/billing/subscribers/invoice-auto-send.js +30 -0
- package/dist/modules/billing/subscribers/invoice-auto-send.js.map +7 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js +69 -33
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +2 -2
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js +4 -6
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +2 -2
- package/generated/entities/billing_account/index.ts +3 -0
- package/generated/entities/billing_allocation/index.ts +18 -0
- package/generated/entities/billing_document_sequence/index.ts +11 -0
- package/generated/entities/billing_invoice/index.ts +38 -0
- package/generated/entities/billing_invoice_line/index.ts +24 -0
- package/generated/entities/billing_item/index.ts +4 -0
- package/generated/entities/billing_payment/index.ts +25 -0
- package/generated/entities/billing_payment_import/index.ts +14 -0
- package/generated/entities/billing_run/index.ts +0 -1
- package/generated/entities/billing_run_outcome/index.ts +1 -0
- package/generated/entities/billing_settings/index.ts +26 -0
- package/generated/entities/billing_statement/index.ts +1 -0
- package/generated/entities.ids.generated.ts +7 -0
- package/package.json +5 -1
- package/src/modules/billing/__integration__/TC-BILL-004.spec.ts +171 -0
- package/src/modules/billing/__integration__/TC-BILL-005.spec.ts +134 -0
- package/src/modules/billing/__integration__/TC-BILL-006.spec.ts +101 -0
- package/src/modules/billing/__integration__/TC-BILL-007.spec.ts +140 -0
- package/src/modules/billing/__integration__/TC-BILL-008.spec.ts +108 -0
- package/src/modules/billing/__integration__/TC-BILL-009.spec.ts +113 -0
- package/src/modules/billing/__integration__/TC-BILL-010.spec.ts +118 -0
- package/src/modules/billing/__integration__/TC-BILL-011.spec.ts +142 -0
- package/src/modules/billing/__integration__/TC-BILL-012.spec.ts +115 -0
- package/src/modules/billing/__integration__/TC-BILL-013.spec.ts +118 -0
- package/src/modules/billing/__integration__/sellerSettings.ts +24 -0
- package/src/modules/billing/__tests__/commands/invoices.test.ts +436 -354
- package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +33 -0
- package/src/modules/billing/__tests__/commands/memos.test.ts +205 -0
- package/src/modules/billing/__tests__/commands/payments.test.ts +241 -0
- package/src/modules/billing/__tests__/commands/runsDelete.test.ts +146 -0
- package/src/modules/billing/__tests__/commands/runsPostDrafts.test.ts +106 -0
- package/src/modules/billing/__tests__/commands/topups.test.ts +11 -0
- package/src/modules/billing/__tests__/entityRegistry.test.ts +8 -1
- package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +28 -11
- package/src/modules/billing/__tests__/i18n/dictionaries.test.ts +64 -0
- package/src/modules/billing/__tests__/lib/accountFinancials.test.ts +53 -0
- package/src/modules/billing/__tests__/lib/accountSummary.test.ts +97 -0
- package/src/modules/billing/__tests__/lib/allocation.test.ts +319 -0
- package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +228 -17
- package/src/modules/billing/__tests__/lib/billingSettings.test.ts +109 -0
- package/src/modules/billing/__tests__/lib/customerLookup.test.ts +156 -0
- package/src/modules/billing/__tests__/lib/dueDate.test.ts +35 -0
- package/src/modules/billing/__tests__/lib/iban.test.ts +38 -0
- package/src/modules/billing/__tests__/lib/invoiceDelivery.test.ts +206 -0
- package/src/modules/billing/__tests__/lib/invoiceLanguages.test.ts +49 -0
- package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +212 -10
- package/src/modules/billing/__tests__/lib/itemBillingState.test.ts +42 -0
- package/src/modules/billing/__tests__/lib/itemPriceChange.test.ts +165 -0
- package/src/modules/billing/__tests__/lib/lineAmounts.test.ts +86 -0
- package/src/modules/billing/__tests__/lib/navigation.test.ts +71 -0
- package/src/modules/billing/__tests__/lib/numbering.test.ts +91 -0
- package/src/modules/billing/__tests__/lib/overdue.test.ts +36 -0
- package/src/modules/billing/__tests__/lib/paymentCsv.test.ts +148 -0
- package/src/modules/billing/__tests__/lib/paymentImport.test.ts +113 -0
- package/src/modules/billing/__tests__/lib/paymentsSample.test.ts +73 -0
- package/src/modules/billing/__tests__/lib/pdf/invoicePdf.test.ts +269 -0
- package/src/modules/billing/__tests__/lib/pdf/invoicePdfService.test.ts +147 -0
- package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +374 -98
- package/src/modules/billing/__tests__/lib/prepaidTaxPoint.test.ts +118 -0
- package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +53 -2
- package/src/modules/billing/__tests__/lib/sellerSettings.test.ts +28 -0
- package/src/modules/billing/__tests__/lib/servicePeriods.test.ts +190 -0
- package/src/modules/billing/__tests__/lib/topupCapture.test.ts +6 -28
- package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +21 -52
- package/src/modules/billing/__tests__/lib/vat.test.ts +51 -0
- package/src/modules/billing/__tests__/package-isolation.test.ts +22 -8
- package/src/modules/billing/__tests__/setup.test.ts +13 -4
- package/src/modules/billing/__tests__/validators.test.ts +197 -3
- package/src/modules/billing/acl.ts +12 -0
- package/src/modules/billing/api/accounts/[id]/summary/route.ts +62 -0
- package/src/modules/billing/api/accounts/route.ts +59 -4
- package/src/modules/billing/api/export/account/[id]/route.ts +13 -11
- package/src/modules/billing/api/invoices/[id]/memos/route.ts +63 -0
- package/src/modules/billing/api/invoices/[id]/pdf/route.ts +80 -0
- package/src/modules/billing/api/invoices/[id]/route.ts +57 -70
- package/src/modules/billing/api/invoices/[id]/send/route.ts +127 -0
- package/src/modules/billing/api/invoices/add-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/delete-draft/route.ts +33 -0
- package/src/modules/billing/api/invoices/edit-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/post/route.ts +7 -6
- package/src/modules/billing/api/invoices/remove-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/route.ts +81 -86
- package/src/modules/billing/api/items/bulk/route.ts +2 -1
- package/src/modules/billing/api/items/change-price/route.ts +54 -0
- package/src/modules/billing/api/items/route.ts +23 -7
- package/src/modules/billing/api/payment-imports/[id]/route.ts +49 -0
- package/src/modules/billing/api/payment-imports/route.ts +57 -0
- package/src/modules/billing/api/payments/[id]/route.ts +63 -0
- package/src/modules/billing/api/payments/allocate/route.ts +35 -0
- package/src/modules/billing/api/payments/assign/route.ts +36 -0
- package/src/modules/billing/api/payments/convert-to-topup/route.ts +35 -0
- package/src/modules/billing/api/payments/ignore/route.ts +35 -0
- package/src/modules/billing/api/payments/import/route.ts +72 -0
- package/src/modules/billing/api/payments/restore/route.ts +35 -0
- package/src/modules/billing/api/payments/route.ts +147 -0
- package/src/modules/billing/api/payments/unallocate/route.ts +35 -0
- package/src/modules/billing/api/payments/unassign/route.ts +35 -0
- package/src/modules/billing/api/run-outcomes/route.ts +49 -6
- package/src/modules/billing/api/runs/[id]/post-drafts/route.ts +71 -0
- package/src/modules/billing/api/runs/[id]/route.ts +59 -0
- package/src/modules/billing/api/runs/retry-failed/route.ts +2 -1
- package/src/modules/billing/api/runs/route.ts +15 -7
- package/src/modules/billing/api/settings/number-preview/route.ts +46 -0
- package/src/modules/billing/api/settings/route.ts +116 -0
- package/src/modules/billing/api/statements/route.ts +5 -0
- package/src/modules/billing/api/test-invoices/wipe/route.ts +2 -2
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +151 -372
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +6 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +70 -4
- package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +444 -95
- package/src/modules/billing/backend/billing/invoices/page.tsx +8 -227
- package/src/modules/billing/backend/billing/items/[id]/page.tsx +75 -6
- package/src/modules/billing/backend/billing/items/create/page.tsx +13 -8
- package/src/modules/billing/backend/billing/items/page.tsx +18 -2
- package/src/modules/billing/backend/billing/payments/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/payments/[id]/page.tsx +415 -0
- package/src/modules/billing/backend/billing/payments/imports/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/payments/imports/[id]/page.tsx +106 -0
- package/src/modules/billing/backend/billing/payments/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/payments/page.tsx +56 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.tsx +49 -18
- package/src/modules/billing/backend/billing/runs/page.tsx +59 -8
- package/src/modules/billing/backend/billing/statements/[id]/page.tsx +86 -5
- package/src/modules/billing/backend/billing/statements/page.tsx +1 -1
- package/src/modules/billing/backend/billing/topups/page.tsx +1 -1
- package/src/modules/billing/backend/billing/transactions/page.tsx +1 -1
- package/src/modules/billing/backend/billing/usage/create/page.tsx +3 -2
- package/src/modules/billing/backend/config/billing/page.meta.ts +21 -0
- package/src/modules/billing/backend/config/billing/page.tsx +549 -0
- package/src/modules/billing/cli.ts +31 -1
- package/src/modules/billing/commands/accounts.ts +6 -0
- package/src/modules/billing/commands/index.ts +3 -0
- package/src/modules/billing/commands/invoices.ts +364 -231
- package/src/modules/billing/commands/items.ts +143 -0
- package/src/modules/billing/commands/memos.ts +218 -0
- package/src/modules/billing/commands/payments.ts +428 -0
- package/src/modules/billing/commands/runs.ts +205 -3
- package/src/modules/billing/commands/settings.ts +77 -0
- package/src/modules/billing/commands/topups.ts +12 -3
- package/src/modules/billing/commands/transactions.ts +2 -2
- package/src/modules/billing/components/AccountForm.tsx +199 -53
- package/src/modules/billing/components/AccountInvoicesSection.tsx +75 -0
- package/src/modules/billing/components/AccountPaymentsSection.tsx +42 -0
- package/src/modules/billing/components/AccountPicker.tsx +70 -0
- package/src/modules/billing/components/AllocatePaymentDialog.tsx +167 -0
- package/src/modules/billing/components/BillingDataTable.tsx +34 -0
- package/src/modules/billing/components/BillingSettingsLink.tsx +25 -0
- package/src/modules/billing/components/ChangeItemPriceDialog.tsx +180 -0
- package/src/modules/billing/components/CreateMemoDialog.tsx +232 -0
- package/src/modules/billing/components/CustomerLabel.tsx +53 -0
- package/src/modules/billing/components/InvoicesTable.tsx +344 -0
- package/src/modules/billing/components/ItemForm.tsx +233 -44
- package/src/modules/billing/components/ItemPriceField.tsx +81 -0
- package/src/modules/billing/components/LineFormDialog.tsx +25 -1
- package/src/modules/billing/components/ManualPaymentDialog.tsx +140 -0
- package/src/modules/billing/components/MoneyInput.tsx +26 -0
- package/src/modules/billing/components/PaymentUploadDialog.tsx +148 -0
- package/src/modules/billing/components/PaymentsTable.tsx +213 -0
- package/src/modules/billing/components/PostRunDraftsButton.tsx +129 -0
- package/src/modules/billing/components/PrepaidPanel.tsx +19 -6
- package/src/modules/billing/components/RunCyclesSection.tsx +298 -0
- package/src/modules/billing/components/TestRunActions.tsx +152 -0
- package/src/modules/billing/components/account/AccountItemsTab.tsx +192 -0
- package/src/modules/billing/components/account/AccountRunBillingDialog.tsx +147 -0
- package/src/modules/billing/components/account/AccountRunsTab.tsx +201 -0
- package/src/modules/billing/components/account/AccountStatementsTab.tsx +120 -0
- package/src/modules/billing/components/account/AccountSummaryCards.tsx +153 -0
- package/src/modules/billing/components/account/AccountUsageTab.tsx +122 -0
- package/src/modules/billing/components/account/AccountView.tsx +101 -0
- package/src/modules/billing/components/account/accountRow.ts +70 -0
- package/src/modules/billing/components/customerNames.ts +62 -0
- package/src/modules/billing/data/entities.ts +656 -6
- package/src/modules/billing/data/entityIds.ts +9 -0
- package/src/modules/billing/data/validators.ts +388 -8
- package/src/modules/billing/events.ts +56 -3
- package/src/modules/billing/i18n/de.json +880 -0
- package/src/modules/billing/i18n/en.json +457 -21
- package/src/modules/billing/i18n/es.json +880 -0
- package/src/modules/billing/i18n/pl.json +458 -22
- package/src/modules/billing/index.ts +6 -2
- package/src/modules/billing/lib/accountFinancials.ts +84 -0
- package/src/modules/billing/lib/accountLock.ts +32 -0
- package/src/modules/billing/lib/accountStatus.ts +11 -9
- package/src/modules/billing/lib/accountSummary.ts +172 -0
- package/src/modules/billing/lib/allocation.ts +515 -0
- package/src/modules/billing/lib/allocationEvents.ts +26 -0
- package/src/modules/billing/lib/billPeriod.ts +19 -0
- package/src/modules/billing/lib/billRunEngine.ts +350 -150
- package/src/modules/billing/lib/billingSettings.ts +145 -0
- package/src/modules/billing/lib/commandRoute.ts +112 -0
- package/src/modules/billing/lib/customerLookup.ts +157 -0
- package/src/modules/billing/lib/dateOnly.ts +22 -0
- package/src/modules/billing/lib/documentParties.ts +23 -0
- package/src/modules/billing/lib/documentTypes.ts +37 -0
- package/src/modules/billing/lib/dueDate.ts +26 -0
- package/src/modules/billing/lib/iban.ts +36 -0
- package/src/modules/billing/lib/invoiceDelivery.ts +196 -0
- package/src/modules/billing/lib/invoiceLanguages.ts +57 -0
- package/src/modules/billing/lib/invoiceWriter.ts +162 -148
- package/src/modules/billing/lib/itemBillingState.ts +67 -0
- package/src/modules/billing/lib/itemPriceChange.ts +56 -0
- package/src/modules/billing/lib/itemSelector.ts +4 -1
- package/src/modules/billing/lib/lineAmounts.ts +61 -0
- package/src/modules/billing/lib/memoGuards.ts +41 -0
- package/src/modules/billing/lib/navigation.ts +64 -0
- package/src/modules/billing/lib/numbering.ts +174 -0
- package/src/modules/billing/lib/overdue.ts +60 -0
- package/src/modules/billing/lib/paymentCsv.ts +171 -0
- package/src/modules/billing/lib/paymentGateway.ts +52 -0
- package/src/modules/billing/lib/paymentImport.ts +228 -0
- package/src/modules/billing/lib/paymentMatcher.ts +88 -0
- package/src/modules/billing/lib/pdf/attachmentStore.ts +60 -0
- package/src/modules/billing/lib/pdf/fonts/NotoSans-Bold.ttf +0 -0
- package/src/modules/billing/lib/pdf/fonts/NotoSans-Regular.ttf +0 -0
- package/src/modules/billing/lib/pdf/fonts/OFL.txt +93 -0
- package/src/modules/billing/lib/pdf/fonts/embeddedFonts.ts +9 -0
- package/src/modules/billing/lib/pdf/invoiceModel.ts +329 -0
- package/src/modules/billing/lib/pdf/invoicePdf.ts +220 -0
- package/src/modules/billing/lib/pdf/invoiceTemplate.ts +338 -0
- package/src/modules/billing/lib/prepaidConfig.ts +27 -36
- package/src/modules/billing/lib/prepaidConsume.ts +29 -6
- package/src/modules/billing/lib/prepaidPeriodClose.ts +407 -239
- package/src/modules/billing/lib/prepaidTaxPoint.ts +76 -0
- package/src/modules/billing/lib/seeds/demo.ts +79 -6
- package/src/modules/billing/lib/seeds/paymentsSample.ts +136 -0
- package/src/modules/billing/lib/seeds/sellerSettings.ts +78 -0
- package/src/modules/billing/lib/servicePeriods.ts +226 -0
- package/src/modules/billing/lib/testRunCleanup.ts +42 -0
- package/src/modules/billing/lib/topupCapture.ts +3 -4
- package/src/modules/billing/lib/topupReceipt.ts +70 -103
- package/src/modules/billing/lib/usageRater.ts +1 -1
- package/src/modules/billing/lib/usageRunner.ts +17 -5
- package/src/modules/billing/lib/vat.ts +56 -0
- package/src/modules/billing/migrations/Migration20261114000000_billing_customer_kind.ts +25 -0
- package/src/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.ts +37 -0
- package/src/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.ts +132 -0
- package/src/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.ts +58 -0
- package/src/modules/billing/migrations/Migration20261117000000_billing_payments.ts +97 -0
- package/src/modules/billing/migrations/Migration20261118000000_billing_memos.ts +21 -0
- package/src/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.ts +39 -0
- package/src/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.ts +35 -0
- package/src/modules/billing/setup.ts +11 -237
- package/src/modules/billing/subscribers/credit-memo-auto-send.ts +15 -0
- package/src/modules/billing/subscribers/debit-memo-apply-credit.ts +15 -0
- package/src/modules/billing/subscribers/debit-memo-auto-send.ts +15 -0
- package/src/modules/billing/subscribers/invoice-apply-credit.ts +39 -0
- package/src/modules/billing/subscribers/invoice-auto-send.ts +46 -0
- package/src/modules/billing/subscribers/payment-captured-to-paid.ts +84 -62
- package/src/modules/billing/subscribers/prepaid-topup-captured.ts +4 -8
- package/dist/modules/billing/lib/invoiceStatus.js +0 -38
- package/dist/modules/billing/lib/invoiceStatus.js.map +0 -7
- package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +0 -97
- package/src/modules/billing/lib/invoiceStatus.ts +0 -61
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/commands/invoices.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 { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { SalesInvoice, SalesInvoiceLine } from '@open-mercato/core/modules/sales/data/entities'\nimport { BillingAccount, BillingDraftInvoiceEdit } from '../data/entities'\nimport { emitBillingEvent, type BillingEventId } from '../events'\nimport { buildPostpaidCreditCrossingEvents } from '../lib/accountStatus'\nimport {\n billingInvoiceAddLineSchema,\n billingInvoiceEditLineSchema,\n billingInvoicePostSchema,\n billingInvoiceRemoveLineSchema,\n billingWipeTestInvoicesSchema,\n type BillingInvoiceAddLineInput,\n type BillingInvoiceEditLineInput,\n type BillingInvoicePostInput,\n type BillingInvoiceRemoveLineInput,\n type BillingWipeTestInvoicesInput,\n} from '../data/validators'\nimport { resolveInvoiceStatusEntryId } from '../lib/invoiceStatus'\n\n/**\n * Posts a `core/sales` draft invoice that the Bill Run engine\n * created.\n *\n * Phase 2 deviation note: the invoice number is already set at\n * draft-create time (because `SalesInvoice.invoice_number` is NOT\n * NULL upstream). \"Post\" therefore only:\n * 1. Asserts the invoice is currently a draft (refuse otherwise).\n * 2. Refuses test-mode invoices (`metadata.test_run=true`) \u2014 they\n * should never reach production posting.\n * 3. Flips `status` (and `status_entry_id`) to `posted`.\n * 4. Fires `billing.invoice.posted` (one) + `billing.invoice.line_posted`\n * per line (for downstream consumers \u2014 accounting, analytics).\n *\n * Once posted, the invoice is frozen at the billing layer; corrections\n * go through `core/sales`'s manual edit flow.\n */\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\ntype PostResult = {\n invoiceId: string\n invoiceNumber: string\n status: 'posted'\n lineCount: number\n}\n\nconst postInvoiceCommand: CommandHandler<BillingInvoicePostInput, PostResult> = {\n id: 'billing.invoices.post',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoicePostSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const invoice = await em.findOne(SalesInvoice, {\n id: parsed.invoiceId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!invoice) {\n throw new CrudHttpError(404, { error: 'Invoice not found' })\n }\n if (invoice.status !== 'draft') {\n throw new CrudHttpError(409, {\n error: 'Only draft invoices can be posted',\n code: 'billing.invoice.not_draft',\n currentStatus: invoice.status,\n })\n }\n const meta = (invoice.metadata as Record<string, unknown> | null) ?? {}\n if (meta.test_run === true) {\n // Test invoices live for inspection only \u2014 posting them would\n // leak test data into accounting. Force-delete the test invoice\n // instead (Phase 4b admin UI exposes a \"Wipe test drafts\"\n // button against `DELETE /api/billing/test-invoices`).\n throw new CrudHttpError(409, {\n error: 'Test-mode invoices cannot be posted',\n code: 'billing.invoice.test_run',\n })\n }\n\n const postedStatusEntryId = await resolveInvoiceStatusEntryId(\n em,\n parsed.tenantId,\n parsed.organizationId,\n 'posted',\n )\n\n await withAtomicFlush(em, [\n () => {\n invoice.status = 'posted'\n invoice.statusEntryId = postedStatusEntryId\n invoice.updatedAt = new Date()\n },\n ])\n\n const lines = await em.find(SalesInvoiceLine, {\n invoice,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n } as never)\n\n // Side effects fire AFTER the DB commit so downstream subscribers\n // (mailer, accounting bridge, analytics) never observe a \"posted\"\n // event for an invoice the DB doesn't reflect.\n await emitBillingEvent('billing.invoice.posted', {\n invoiceId: invoice.id,\n invoiceNumber: invoice.invoiceNumber,\n tenantId: invoice.tenantId,\n organizationId: invoice.organizationId,\n grandTotalNet: invoice.grandTotalNetAmount,\n grandTotalGross: invoice.grandTotalGrossAmount,\n currencyCode: invoice.currencyCode,\n billRunId: (meta as { bill_run_id?: string }).bill_run_id ?? null,\n billAccountId: (meta as { bill_account_id?: string }).bill_account_id ?? null,\n billPeriodStart: (meta as { bill_period_start?: string }).bill_period_start ?? null,\n billPeriodEnd: (meta as { bill_period_end?: string }).bill_period_end ?? null,\n lineCount: lines.length,\n })\n for (const line of lines) {\n const lineMeta = (line.metadata as Record<string, unknown> | null) ?? {}\n await emitBillingEvent('billing.invoice.line_posted', {\n invoiceId: invoice.id,\n invoiceLineId: line.id,\n billingItemId: lineMeta.billing_item_id ?? null,\n billingType: lineMeta.billing_type ?? null,\n description: line.description ?? null,\n unitPriceNet: line.unitPriceNet,\n quantity: line.quantity,\n totalNetAmount: line.totalNetAmount,\n tenantId: invoice.tenantId,\n organizationId: invoice.organizationId,\n })\n }\n\n // SPEC-002 P6: a newly-posted invoice raises the account's outstanding\n // exposure \u2014 emit billing.credit.over_limit on the upward crossing. Billing\n // never blocks; the consuming flow decides what to do.\n const billAccountId = (meta as { bill_account_id?: string }).bill_account_id\n if (billAccountId) {\n const account = await em.findOne(BillingAccount, {\n id: billAccountId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (account) {\n const creditEvents = await buildPostpaidCreditCrossingEvents(\n em,\n account,\n invoice.outstandingAmount,\n )\n for (const ev of creditEvents) {\n await emitBillingEvent(ev.id as BillingEventId, ev.payload, { persistent: true })\n }\n }\n }\n\n return {\n invoiceId: invoice.id,\n invoiceNumber: invoice.invoiceNumber,\n status: 'posted',\n lineCount: lines.length,\n }\n },\n}\n\n// \u2500\u2500\u2500 Draft edit / add / remove (Phase 4b) \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// Three commands sharing the same shape:\n// 1. Assert the invoice is still a draft (refuse otherwise).\n// 2. Mutate the line / collection.\n// 3. Write a `BillingDraftInvoiceEdit` audit row carrying before+after\n// snapshots (`null` on the \"no prior state\" / \"no resulting state\"\n// side per the spec \u2014 see entity column docs).\n// 4. Recompute the invoice totals from the surviving lines.\n//\n// The audit row is append-only; once written, never updated. Soft-\n// deleting the invoice does NOT delete the audit rows (compliance).\n\nfunction roundHalfUp2dp(value: number): number {\n return Math.round((value + Number.EPSILON) * 100) / 100\n}\n\nfunction format4dp(value: number): string {\n return roundHalfUp2dp(value).toFixed(4)\n}\n\nasync function assertDraftInvoice(\n em: EntityManager,\n params: { invoiceId: string; tenantId: string; organizationId: string },\n): Promise<SalesInvoice> {\n const invoice = await em.findOne(SalesInvoice, {\n id: params.invoiceId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n deletedAt: null,\n })\n if (!invoice) {\n throw new CrudHttpError(404, { error: 'Invoice not found' })\n }\n if (invoice.status !== 'draft') {\n throw new CrudHttpError(409, {\n error: 'Only draft invoices can be edited',\n code: 'billing.invoice.not_draft',\n currentStatus: invoice.status,\n })\n }\n return invoice\n}\n\ntype LineSnapshot = {\n id: string\n description: string | null\n quantity: string\n unitPriceNet: string\n totalNetAmount: string\n totalGrossAmount: string\n metadata: Record<string, unknown> | null\n}\n\nfunction snapshotLine(line: SalesInvoiceLine): LineSnapshot {\n return {\n id: line.id,\n description: line.description ?? null,\n quantity: line.quantity,\n unitPriceNet: line.unitPriceNet,\n totalNetAmount: line.totalNetAmount,\n totalGrossAmount: line.totalGrossAmount,\n metadata: (line.metadata as Record<string, unknown> | null) ?? null,\n }\n}\n\nasync function recomputeInvoiceTotals(\n em: EntityManager,\n invoice: SalesInvoice,\n): Promise<void> {\n const lines = await em.find(SalesInvoiceLine, {\n invoice,\n tenantId: invoice.tenantId,\n organizationId: invoice.organizationId,\n } as never)\n let totalNet = 0\n for (const line of lines) {\n totalNet += Number.parseFloat(line.totalNetAmount)\n }\n const formatted = format4dp(totalNet)\n invoice.subtotalNetAmount = formatted\n invoice.subtotalGrossAmount = formatted\n invoice.grandTotalNetAmount = formatted\n invoice.grandTotalGrossAmount = formatted\n invoice.outstandingAmount = formatted\n invoice.updatedAt = new Date()\n}\n\nfunction getUserIdFromCtx(ctx: CommandRuntimeContext): string {\n const userId = (ctx.auth?.sub as string | undefined) ?? null\n // The BillingDraftInvoiceEdit.user_id column is NOT NULL. For system-driven\n // edits (e.g. catch-up flow that revises a value), we fall back to\n // a sentinel UUID. Operator-driven edits always have a real\n // ctx.auth.sub.\n return userId ?? '00000000-0000-0000-0000-000000000000'\n}\n\nconst editDraftLineCommand: CommandHandler<\n BillingInvoiceEditLineInput,\n { invoiceId: string; invoiceLineId: string; auditId: string }\n> = {\n id: 'billing.invoices.edit_draft_line',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoiceEditLineSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const invoice = await assertDraftInvoice(em, {\n invoiceId: parsed.invoiceId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n\n const line = await em.findOne(SalesInvoiceLine, {\n id: parsed.invoiceLineId,\n invoice,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n if (!line) {\n throw new CrudHttpError(404, { error: 'Invoice line not found' })\n }\n\n const before = snapshotLine(line)\n let audit: BillingDraftInvoiceEdit | null = null\n\n await withAtomicFlush(em, [\n () => {\n if (parsed.changes.description !== undefined) {\n line.description = parsed.changes.description\n }\n if (parsed.changes.quantity !== undefined) {\n line.quantity = format4dp(parsed.changes.quantity)\n line.normalizedQuantity = line.quantity\n }\n if (parsed.changes.unitPriceNet !== undefined) {\n line.unitPriceNet = format4dp(parsed.changes.unitPriceNet)\n line.unitPriceGross = line.unitPriceNet\n }\n\n // Recompute the line total. If the operator supplied an explicit\n // `totalNetAmount` override, honour it; otherwise compute from\n // unit_price \u00D7 quantity.\n if (parsed.changes.totalNetAmount !== undefined) {\n const total = format4dp(parsed.changes.totalNetAmount)\n line.totalNetAmount = total\n line.totalGrossAmount = total\n } else {\n const total = format4dp(\n Number.parseFloat(line.unitPriceNet) * Number.parseFloat(line.quantity),\n )\n line.totalNetAmount = total\n line.totalGrossAmount = total\n }\n },\n async () => {\n await recomputeInvoiceTotals(em, invoice)\n },\n () => {\n const after = snapshotLine(line)\n audit = em.create(BillingDraftInvoiceEdit, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n invoiceId: invoice.id,\n invoiceLineId: line.id,\n userId: getUserIdFromCtx(ctx),\n action: 'line_edited',\n beforeJson: before as Record<string, unknown>,\n afterJson: after as Record<string, unknown>,\n createdAt: new Date(),\n })\n em.persist(audit)\n },\n ])\n\n return { invoiceId: invoice.id, invoiceLineId: line.id, auditId: audit!.id }\n },\n}\n\nconst addDraftLineCommand: CommandHandler<\n BillingInvoiceAddLineInput,\n { invoiceId: string; invoiceLineId: string; auditId: string }\n> = {\n id: 'billing.invoices.add_draft_line',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoiceAddLineSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const invoice = await assertDraftInvoice(em, {\n invoiceId: parsed.invoiceId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n\n const existing = await em.find(SalesInvoiceLine, {\n invoice,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n } as never)\n const nextLineNumber = existing.length + 1\n\n const total = format4dp(parsed.unitPriceNet * parsed.quantity)\n const line = em.create(SalesInvoiceLine, {\n invoice,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n lineNumber: nextLineNumber,\n kind: 'product',\n description: parsed.description,\n quantity: format4dp(parsed.quantity),\n normalizedQuantity: format4dp(parsed.quantity),\n currencyCode: invoice.currencyCode,\n unitPriceNet: format4dp(parsed.unitPriceNet),\n unitPriceGross: format4dp(parsed.unitPriceNet),\n discountAmount: '0',\n discountPercent: '0',\n taxRate: '0',\n taxAmount: '0',\n totalNetAmount: total,\n totalGrossAmount: total,\n metadata: {\n billing_item_id: parsed.billingItemId ?? null,\n billing_type: parsed.billingType ?? 'manual',\n operator_added: true,\n },\n })\n let audit: BillingDraftInvoiceEdit | null = null\n\n await withAtomicFlush(em, [\n () => {\n em.persist(line)\n },\n async () => {\n await recomputeInvoiceTotals(em, invoice)\n },\n () => {\n const after = snapshotLine(line)\n audit = em.create(BillingDraftInvoiceEdit, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n invoiceId: invoice.id,\n invoiceLineId: line.id,\n userId: getUserIdFromCtx(ctx),\n action: 'line_added',\n beforeJson: null,\n afterJson: after as Record<string, unknown>,\n createdAt: new Date(),\n })\n em.persist(audit)\n },\n ])\n\n return { invoiceId: invoice.id, invoiceLineId: line.id, auditId: audit!.id }\n },\n}\n\nconst removeDraftLineCommand: CommandHandler<\n BillingInvoiceRemoveLineInput,\n { invoiceId: string; invoiceLineId: string; auditId: string }\n> = {\n id: 'billing.invoices.remove_draft_line',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoiceRemoveLineSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const invoice = await assertDraftInvoice(em, {\n invoiceId: parsed.invoiceId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n const line = await em.findOne(SalesInvoiceLine, {\n id: parsed.invoiceLineId,\n invoice,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n if (!line) {\n throw new CrudHttpError(404, { error: 'Invoice line not found' })\n }\n\n const before = snapshotLine(line)\n let audit: BillingDraftInvoiceEdit | null = null\n\n await withAtomicFlush(em, [\n () => {\n em.remove(line)\n },\n async () => {\n await recomputeInvoiceTotals(em, invoice)\n },\n () => {\n audit = em.create(BillingDraftInvoiceEdit, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n invoiceId: invoice.id,\n invoiceLineId: null,\n userId: getUserIdFromCtx(ctx),\n action: 'line_removed',\n beforeJson: before as Record<string, unknown>,\n afterJson: null,\n createdAt: new Date(),\n })\n em.persist(audit)\n },\n ])\n\n return { invoiceId: invoice.id, invoiceLineId: parsed.invoiceLineId, auditId: audit!.id }\n },\n}\n\n// \u2500\u2500\u2500 Wipe test invoices (Phase 4b) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Hard-deletes invoices flagged `metadata.test_run=true` (and their\n// lines via cascade \u2014 the invoice's lines collection points at it\n// with ON DELETE CASCADE). Optionally scoped to a single `bill_run_id`\n// via the param so operators can wipe a specific test run without\n// risking other test data.\n\nconst wipeTestInvoicesCommand: CommandHandler<\n BillingWipeTestInvoicesInput,\n { invoicesRemoved: number }\n> = {\n id: 'billing.invoices.wipe_test',\n\n async execute(rawInput, ctx) {\n const parsed = billingWipeTestInvoicesSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n // The metadata->>'test_run' = 'true' predicate uses Postgres'\n // JSONB ->> operator + the literal string 'true' (jsonb true is\n // serialized as the JSON literal `true`, which ->> returns as the\n // text \"true\").\n const params: unknown[] = [parsed.tenantId, parsed.organizationId]\n let billRunClause = ''\n if (parsed.billRunId) {\n billRunClause = ` AND metadata->>'bill_run_id' = ?`\n params.push(parsed.billRunId)\n }\n const rows = (await em.execute(\n `SELECT id FROM sales_invoices\n WHERE tenant_id = ?\n AND organization_id = ?\n AND deleted_at IS NULL\n AND metadata->>'test_run' = 'true'${billRunClause}`,\n params,\n )) as unknown as Array<{ id: string }> | undefined\n if (!rows || rows.length === 0) {\n return { invoicesRemoved: 0 }\n }\n const ids = rows.map((r) => r.id)\n // Hard-delete lines first then invoices to avoid relying on\n // cascade semantics that the migration may or may not encode.\n await em.execute(\n `DELETE FROM sales_invoice_lines WHERE invoice_id IN (${ids.map(() => '?').join(',')})`,\n ids,\n )\n await em.execute(\n `DELETE FROM sales_invoices WHERE id IN (${ids.map(() => '?').join(',')})`,\n ids,\n )\n return { invoicesRemoved: ids.length }\n },\n}\n\nregisterCommand(postInvoiceCommand)\nregisterCommand(editDraftLineCommand)\nregisterCommand(addDraftLineCommand)\nregisterCommand(removeDraftLineCommand)\nregisterCommand(wipeTestInvoicesCommand)\n\nexport {\n postInvoiceCommand,\n editDraftLineCommand,\n addDraftLineCommand,\n removeDraftLineCommand,\n wipeTestInvoicesCommand,\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { LockMode, UniqueConstraintViolationException } from '@mikro-orm/core'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n BillingAccount,\n BillingDraftInvoiceEdit,\n BillingInvoice,\n BillingInvoiceLine,\n} from '../data/entities'\nimport { emitBillingEvent, type BillingEventId } from '../events'\nimport { buildPostpaidCreditCrossingEvents } from '../lib/accountStatus'\nimport { buildSellerSnapshot, getBillingSettings, missingSellerFields, numberMaskFor, taxRateColumn } from '../lib/billingSettings'\nimport { claimDocumentNumber } from '../lib/numbering'\nimport { buildBuyerSnapshot } from '../lib/documentParties'\nimport { freezeInvoicePdf } from '../lib/pdf/invoicePdf'\nimport { accountLockKey } from '../lib/accountLock'\nimport { allocate, applyAccountCredit, emptyEffects, mergeEffects, toCents } from '../lib/allocation'\nimport { emitAllocationEffects } from '../lib/allocationEvents'\nimport { seriesFor } from '../lib/documentTypes'\nimport { creditFits, loadCorrectionTotals } from '../lib/memoGuards'\nimport { sendInvoice, type SendInvoiceResult } from '../lib/invoiceDelivery'\nimport { computeDueDate, resolvePaymentTermsDays } from '../lib/dueDate'\nimport { lineAmountsFromNet, sumTotals } from '../lib/vat'\nimport { deleteTestRunArtifacts } from '../lib/testRunCleanup'\nimport {\n billingInvoiceAddLineSchema,\n billingInvoiceEditLineSchema,\n billingInvoicePostSchema,\n billingInvoiceRemoveLineSchema,\n billingInvoiceSendSchema,\n billingWipeTestInvoicesSchema,\n type BillingInvoiceAddLineInput,\n type BillingInvoiceEditLineInput,\n type BillingInvoicePostInput,\n type BillingInvoiceRemoveLineInput,\n type BillingInvoiceSendInput,\n type BillingWipeTestInvoicesInput,\n} from '../data/validators'\n\n/**\n * Billing invoice commands (Spec 0 \u2014 billing-owned `billing_invoices`).\n *\n * Post:\n * 1. Refuses non-drafts and test-run drafts.\n * 2. Refuses when the seller settings are incomplete (Spec B, P-D4 \u2014 a\n * posted invoice must be printable); no number is consumed.\n * 3. In ONE transaction: claims the next number of the `invoice` series\n * (gap-free \u2014 a rollback releases it), freezes the buyer / seller\n * snapshots, sets `issue_date`, `due_date` (issue + payment terms),\n * `posted_at`, `outstanding = total_gross`, `status = posted`, and keeps\n * the replaced `DRAFT-\u2026` number in `metadata.draft_number`.\n * 4. After commit: freezes the PDF as an attachment (best effort), then\n * fires `billing.invoice.posted` + one\n * `billing.invoice.line_posted` per line, then the postpaid credit\n * crossing events.\n *\n * Once posted a document is immutable; corrections are credit / debit memos\n * (Spec D). Draft lines can be edited / added / removed with an audit row.\n */\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\ntype PostResult = {\n invoiceId: string\n invoiceNumber: string\n status: 'posted'\n lineCount: number\n}\n\nfunction isoDate(date: Date | null | undefined): string | null {\n return date ? new Date(date).toISOString().slice(0, 10) : null\n}\n\nfunction todayUtc(): Date {\n const now = new Date()\n return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))\n}\n\n// Re-exported for existing callers; lives in lib/documentParties.ts (shared with the PDF).\nexport { buildBuyerSnapshot }\n\nconst postInvoiceCommand: CommandHandler<BillingInvoicePostInput, PostResult> = {\n id: 'billing.invoices.post',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoicePostSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n\n const em = getEm(ctx)\n const invoice = await em.findOne(BillingInvoice, {\n id: parsed.invoiceId,\n ...scope,\n deletedAt: null,\n } as never)\n if (!invoice) {\n throw new CrudHttpError(404, { error: 'Invoice not found' })\n }\n if (invoice.status !== 'draft') {\n throw new CrudHttpError(409, {\n error: 'Only draft invoices can be posted',\n code: 'billing.invoice.not_draft',\n currentStatus: invoice.status,\n })\n }\n if (invoice.testRun) {\n // Test invoices live for inspection only \u2014 posting them would leak test\n // data into accounting. Wipe them instead (POST /api/billing/test-invoices/wipe).\n throw new CrudHttpError(409, {\n error: 'Test-mode invoices cannot be posted',\n code: 'billing.invoice.test_run',\n })\n }\n\n const account = await em.findOne(BillingAccount, {\n id: invoice.billAccountId,\n ...scope,\n } as never)\n if (!account) {\n throw new CrudHttpError(409, { error: 'Billing account not found', code: 'billing.invoice.account_missing' })\n }\n const settings = await getBillingSettings(em, scope)\n const missing = missingSellerFields(settings)\n if (missing.length > 0) {\n throw new CrudHttpError(409, {\n error: 'Fill in the seller details in billing settings before posting invoices',\n code: 'billing.settings.incomplete',\n missing,\n })\n }\n const isCreditMemo = invoice.documentType === 'credit_memo'\n const series = seriesFor(invoice.documentType)\n const issueDate = todayUtc()\n // Receivables (invoices, debit memos) are due after the terms; a credit\n // memo is a credit source with no due date (Spec D).\n const dueDate = isCreditMemo\n ? null\n : computeDueDate(issueDate, resolvePaymentTermsDays(account.paymentTermsDays, settings.defaultPaymentTermsDays))\n const settlement = emptyEffects()\n\n try {\n await em.transactional(async (tem) => {\n // Re-read inside the transaction and lock the row, so two concurrent\n // posts of the same draft can't both claim a number.\n const target = await (tem as EntityManager).findOne(\n BillingInvoice,\n { id: invoice.id, ...scope, deletedAt: null } as never,\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n )\n if (!target || target.status !== 'draft') {\n throw new CrudHttpError(409, {\n error: 'Only draft invoices can be posted',\n code: 'billing.invoice.not_draft',\n currentStatus: target?.status ?? null,\n })\n }\n // Spec C/D: every receivables change of an account is serialised by\n // the per-account lock (same key as `withAccountLock`).\n await (tem as EntityManager).execute('SELECT pg_advisory_xact_lock(hashtextextended(?, 0))', [\n accountLockKey({ tenantId: scope.tenantId, billAccountId: target.billAccountId }),\n ])\n let original: BillingInvoice | null = null\n if (isCreditMemo) {\n original = target.correctsInvoiceId\n ? await (tem as EntityManager).findOne(BillingInvoice, { id: target.correctsInvoiceId, ...scope, deletedAt: null } as never)\n : null\n if (!original) {\n throw new CrudHttpError(409, { error: 'The corrected invoice no longer exists', code: 'billing.memo.invalid_original' })\n }\n // Cumulative guard (P-D4), authoritative here under the lock.\n const corrections = await loadCorrectionTotals(tem as EntityManager, scope, original.id, { excludeId: target.id })\n if (!creditFits(toCents(original.totalGross), corrections, toCents(target.totalGross))) {\n throw new CrudHttpError(409, {\n error: 'The credit exceeds what is left to credit on this invoice',\n code: 'billing.memo.exceeds_invoice',\n })\n }\n }\n const claimed = await claimDocumentNumber(\n tem as EntityManager,\n scope,\n series,\n numberMaskFor(settings, series),\n issueDate,\n )\n const now = new Date()\n target.metadata = { ...(target.metadata ?? {}), draft_number: target.number }\n target.number = claimed.number\n target.series = series\n target.issueDate = issueDate\n target.dueDate = dueDate\n target.postedAt = now\n target.buyerSnapshot = buildBuyerSnapshot(account)\n target.sellerSnapshot = buildSellerSnapshot(settings)\n target.status = 'posted'\n target.paidTotal = '0'\n target.outstanding = target.totalGross\n target.updatedAt = now\n await tem.flush()\n if (isCreditMemo && original) {\n // Settle the corrected invoice first (P-D5), the remainder becomes\n // account credit spent on other open receivables (D12).\n const source = { type: 'credit_memo' as const, id: target.id }\n const take = Math.min(toCents(target.totalGross), toCents(original.outstanding))\n if (take > 0 && ['posted', 'partially_paid'].includes(original.status)) {\n mergeEffects(\n settlement,\n await allocate(tem as EntityManager, scope, { source, invoiceId: original.id, amountCents: take, by: 'credit' }),\n )\n }\n mergeEffects(\n settlement,\n await applyAccountCredit(tem as EntityManager, scope, { id: target.billAccountId, currencyCode: target.currencyCode }, {\n onlySources: [source],\n }),\n )\n const fresh = (await (tem as EntityManager).execute(\n `SELECT status, outstanding FROM billing_invoices WHERE id = ?`,\n [target.id],\n )) as unknown as Array<{ status: string; outstanding: string }>\n if (fresh?.[0]) {\n target.status = fresh[0].status\n target.outstanding = fresh[0].outstanding\n }\n }\n Object.assign(invoice, {\n number: target.number,\n series: target.series,\n issueDate: target.issueDate,\n dueDate: target.dueDate,\n metadata: target.metadata,\n buyerSnapshot: target.buyerSnapshot,\n sellerSnapshot: target.sellerSnapshot,\n postedAt: target.postedAt,\n status: target.status,\n outstanding: target.outstanding,\n })\n })\n } catch (err) {\n if (err instanceof UniqueConstraintViolationException) {\n // The mask produced a number already issued (e.g. after a mask change\n // within a period). The transaction rolled back; nothing was posted.\n throw new CrudHttpError(409, {\n error: 'The invoice number mask produced a number that is already in use',\n code: 'billing.invoice.number_conflict',\n })\n }\n throw err\n }\n\n const lines = await em.find(BillingInvoiceLine, {\n invoiceId: invoice.id,\n ...scope,\n deletedAt: null,\n } as never)\n\n // Spec B: freeze the PDF after the commit, before announcing the post (the\n // auto-send subscriber attaches it). A render / storage failure never rolls\n // the post back \u2014 the first download renders and freezes it instead.\n try {\n await freezeInvoicePdf(em, invoice)\n } catch (err) {\n console.error('[billing.invoices.post] PDF could not be frozen; it will be rendered on first download', invoice.id, err)\n }\n\n // Side effects fire AFTER the DB commit so downstream subscribers never\n // observe a \"posted\" event for an invoice the DB doesn't reflect.\n // `billing.invoice.posted` is for invoices only; memos have their own\n // events with the same payload (Spec D, P-D9).\n const postedEvent: BillingEventId =\n invoice.documentType === 'credit_memo'\n ? 'billing.credit_memo.posted'\n : invoice.documentType === 'debit_memo'\n ? 'billing.debit_memo.posted'\n : 'billing.invoice.posted'\n await emitAllocationEffects(scope, settlement)\n await emitBillingEvent(postedEvent, {\n correctsInvoiceId: invoice.correctsInvoiceId ?? null,\n invoiceId: invoice.id,\n invoiceNumber: invoice.number,\n documentType: invoice.documentType,\n tenantId: invoice.tenantId,\n organizationId: invoice.organizationId,\n grandTotalNet: invoice.subtotalNet,\n grandTotalGross: invoice.totalGross,\n currencyCode: invoice.currencyCode,\n billRunId: invoice.billRunId ?? null,\n billAccountId: invoice.billAccountId,\n billDate: isoDate(invoice.billDate),\n billPeriodStart: isoDate(invoice.periodStart),\n billPeriodEnd: isoDate(invoice.periodEnd),\n issueDate: isoDate(invoice.issueDate),\n dueDate: isoDate(invoice.dueDate),\n lineCount: lines.length,\n })\n for (const line of lines) {\n await emitBillingEvent('billing.invoice.line_posted', {\n invoiceId: invoice.id,\n invoiceLineId: line.id,\n billingItemId: line.billingItemId ?? null,\n billingType: line.billingType,\n description: line.description,\n unitPriceNet: line.unitPriceNet,\n quantity: line.quantity,\n totalNetAmount: line.netAmount,\n tenantId: invoice.tenantId,\n organizationId: invoice.organizationId,\n })\n }\n\n // SPEC-002 P6: a newly-posted invoice raises the account's outstanding\n // exposure \u2014 emit billing.credit.over_limit on the upward crossing.\n // Billing never blocks; the consuming flow decides what to do.\n if (!isCreditMemo) {\n const creditEvents = await buildPostpaidCreditCrossingEvents(em, account, invoice.outstanding)\n for (const ev of creditEvents) {\n await emitBillingEvent(ev.id as BillingEventId, ev.payload, { persistent: true })\n }\n }\n\n return {\n invoiceId: invoice.id,\n invoiceNumber: invoice.number,\n status: 'posted',\n lineCount: lines.length,\n }\n },\n}\n\n// \u2500\u2500\u2500 Draft edit / add / remove \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Three commands sharing the same shape:\n// 1. Assert the invoice is still a draft (refuse otherwise).\n// 2. Mutate the line / collection.\n// 3. Write a `BillingDraftInvoiceEdit` audit row carrying before+after\n// snapshots (`null` on the \"no prior state\" / \"no resulting state\" side).\n// 4. Recompute the invoice totals from the surviving lines.\n//\n// The audit row is append-only; once written, never updated.\n\nfunction roundHalfUp2dp(value: number): number {\n const sign = value < 0 ? -1 : 1\n return (sign * Math.round((Math.abs(value) + Number.EPSILON) * 100)) / 100\n}\n\nfunction format4dp(value: number): string {\n return roundHalfUp2dp(value).toFixed(4)\n}\n\nasync function assertDraftInvoice(\n em: EntityManager,\n params: { invoiceId: string; tenantId: string; organizationId: string },\n): Promise<BillingInvoice> {\n const invoice = await em.findOne(BillingInvoice, {\n id: params.invoiceId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n deletedAt: null,\n } as never)\n if (!invoice) {\n throw new CrudHttpError(404, { error: 'Invoice not found' })\n }\n if (invoice.status !== 'draft') {\n throw new CrudHttpError(409, {\n error: 'Only draft invoices can be edited',\n code: 'billing.invoice.not_draft',\n currentStatus: invoice.status,\n })\n }\n return invoice\n}\n\ntype LineSnapshot = {\n id: string\n description: string\n quantity: string\n unitPriceNet: string\n taxRate: string\n netAmount: string\n taxAmount: string\n grossAmount: string\n metadata: Record<string, unknown> | null\n}\n\nfunction snapshotLine(line: BillingInvoiceLine): LineSnapshot {\n return {\n id: line.id,\n description: line.description,\n quantity: line.quantity,\n unitPriceNet: line.unitPriceNet,\n taxRate: line.taxRate,\n netAmount: line.netAmount,\n taxAmount: line.taxAmount,\n grossAmount: line.grossAmount,\n metadata: line.metadata ?? null,\n }\n}\n\n/** Sets net / tax / gross on a line from its net amount and tax rate. */\nfunction applyLineAmounts(line: BillingInvoiceLine, net: number): void {\n const amounts = lineAmountsFromNet(net, Number.parseFloat(line.taxRate) || 0)\n line.netAmount = format4dp(amounts.net)\n line.taxAmount = format4dp(amounts.tax)\n line.grossAmount = format4dp(amounts.gross)\n}\n\nasync function recomputeInvoiceTotals(em: EntityManager, invoice: BillingInvoice): Promise<void> {\n const lines = await em.find(BillingInvoiceLine, {\n invoiceId: invoice.id,\n tenantId: invoice.tenantId,\n organizationId: invoice.organizationId,\n deletedAt: null,\n } as never)\n const totals = sumTotals(\n lines.map((line) => ({\n net: Number.parseFloat(line.netAmount),\n tax: Number.parseFloat(line.taxAmount),\n gross: Number.parseFloat(line.grossAmount),\n })),\n )\n invoice.subtotalNet = totals.subtotalNet\n invoice.taxTotal = totals.taxTotal\n invoice.totalGross = totals.totalGross\n invoice.outstanding = totals.totalGross\n invoice.updatedAt = new Date()\n}\n\nfunction getUserIdFromCtx(ctx: CommandRuntimeContext): string {\n const userId = (ctx.auth?.sub as string | undefined) ?? null\n // `BillingDraftInvoiceEdit.user_id` is NOT NULL. System-driven edits fall\n // back to a sentinel UUID; operator edits always have ctx.auth.sub.\n return userId ?? '00000000-0000-0000-0000-000000000000'\n}\n\nconst editDraftLineCommand: CommandHandler<\n BillingInvoiceEditLineInput,\n { invoiceId: string; invoiceLineId: string; auditId: string }\n> = {\n id: 'billing.invoices.edit_draft_line',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoiceEditLineSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const invoice = await assertDraftInvoice(em, parsed)\n\n const line = await em.findOne(BillingInvoiceLine, {\n id: parsed.invoiceLineId,\n invoiceId: invoice.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n } as never)\n if (!line) {\n throw new CrudHttpError(404, { error: 'Invoice line not found' })\n }\n\n const before = snapshotLine(line)\n let audit: BillingDraftInvoiceEdit | null = null\n\n await withAtomicFlush(em, [\n () => {\n if (parsed.changes.description !== undefined) line.description = parsed.changes.description\n if (parsed.changes.quantity !== undefined) line.quantity = format4dp(parsed.changes.quantity)\n if (parsed.changes.unitPriceNet !== undefined) line.unitPriceNet = format4dp(parsed.changes.unitPriceNet)\n if (parsed.changes.taxRate !== undefined) line.taxRate = taxRateColumn(parsed.changes.taxRate)\n // An explicit total override wins; otherwise unit price \u00D7 quantity.\n const net =\n parsed.changes.totalNetAmount !== undefined\n ? parsed.changes.totalNetAmount\n : Number.parseFloat(line.unitPriceNet) * Number.parseFloat(line.quantity)\n applyLineAmounts(line, net)\n line.updatedAt = new Date()\n },\n async () => {\n await recomputeInvoiceTotals(em, invoice)\n },\n () => {\n audit = em.create(BillingDraftInvoiceEdit, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n invoiceId: invoice.id,\n invoiceLineId: line.id,\n userId: getUserIdFromCtx(ctx),\n action: 'line_edited',\n beforeJson: before as Record<string, unknown>,\n afterJson: snapshotLine(line) as Record<string, unknown>,\n createdAt: new Date(),\n })\n em.persist(audit)\n },\n ])\n\n return { invoiceId: invoice.id, invoiceLineId: line.id, auditId: audit!.id }\n },\n}\n\nconst addDraftLineCommand: CommandHandler<\n BillingInvoiceAddLineInput,\n { invoiceId: string; invoiceLineId: string; auditId: string }\n> = {\n id: 'billing.invoices.add_draft_line',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoiceAddLineSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const invoice = await assertDraftInvoice(em, parsed)\n\n const existing = await em.find(BillingInvoiceLine, {\n invoiceId: invoice.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n } as never)\n const nextLineNumber = existing.reduce((max, l) => Math.max(max, l.lineNumber), 0) + 1\n\n const now = new Date()\n const line = em.create(BillingInvoiceLine, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n invoiceId: invoice.id,\n lineNumber: nextLineNumber,\n billingItemId: parsed.billingItemId ?? null,\n billingType: parsed.billingType ?? 'manual',\n description: parsed.description,\n quantity: format4dp(parsed.quantity),\n unitPriceNet: format4dp(parsed.unitPriceNet),\n // Spec B: explicit rate, else the organization default.\n taxRate:\n parsed.taxRate !== undefined\n ? taxRateColumn(parsed.taxRate)\n : (await getBillingSettings(em, parsed)).defaultTaxRate,\n netAmount: '0',\n taxAmount: '0',\n grossAmount: '0',\n metadata: { operator_added: true },\n createdAt: now,\n updatedAt: now,\n })\n applyLineAmounts(line, parsed.unitPriceNet * parsed.quantity)\n let audit: BillingDraftInvoiceEdit | null = null\n\n await withAtomicFlush(em, [\n () => {\n em.persist(line)\n },\n async () => {\n await recomputeInvoiceTotals(em, invoice)\n },\n () => {\n audit = em.create(BillingDraftInvoiceEdit, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n invoiceId: invoice.id,\n invoiceLineId: line.id,\n userId: getUserIdFromCtx(ctx),\n action: 'line_added',\n beforeJson: null,\n afterJson: snapshotLine(line) as Record<string, unknown>,\n createdAt: new Date(),\n })\n em.persist(audit)\n },\n ])\n\n return { invoiceId: invoice.id, invoiceLineId: line.id, auditId: audit!.id }\n },\n}\n\nconst removeDraftLineCommand: CommandHandler<\n BillingInvoiceRemoveLineInput,\n { invoiceId: string; invoiceLineId: string; auditId: string }\n> = {\n id: 'billing.invoices.remove_draft_line',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoiceRemoveLineSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const invoice = await assertDraftInvoice(em, parsed)\n const line = await em.findOne(BillingInvoiceLine, {\n id: parsed.invoiceLineId,\n invoiceId: invoice.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n } as never)\n if (!line) {\n throw new CrudHttpError(404, { error: 'Invoice line not found' })\n }\n\n const before = snapshotLine(line)\n let audit: BillingDraftInvoiceEdit | null = null\n\n await withAtomicFlush(em, [\n () => {\n em.remove(line)\n },\n async () => {\n await recomputeInvoiceTotals(em, invoice)\n },\n () => {\n audit = em.create(BillingDraftInvoiceEdit, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n invoiceId: invoice.id,\n invoiceLineId: null,\n userId: getUserIdFromCtx(ctx),\n action: 'line_removed',\n beforeJson: before as Record<string, unknown>,\n afterJson: null,\n createdAt: new Date(),\n })\n em.persist(audit)\n },\n ])\n\n return { invoiceId: invoice.id, invoiceLineId: parsed.invoiceLineId, auditId: audit!.id }\n },\n}\n\n// \u2500\u2500\u2500 Wipe test invoices \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Hard-deletes test-run drafts (`test_run = true`) with their lines and\n// draft-edit audit rows (`lib/testRunCleanup`), optionally scoped to one Bill\n// Run so an operator can wipe a specific test run without touching other\n// test data.\n\nconst wipeTestInvoicesCommand: CommandHandler<BillingWipeTestInvoicesInput, { invoicesRemoved: number }> = {\n id: 'billing.invoices.wipe_test',\n\n async execute(rawInput, ctx) {\n const parsed = billingWipeTestInvoicesSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const removed = await em.transactional((tem) =>\n deleteTestRunArtifacts(tem as EntityManager, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n billRunId: parsed.billRunId ?? null,\n }),\n )\n return { invoicesRemoved: removed }\n },\n}\n\nregisterCommand(postInvoiceCommand)\nregisterCommand(editDraftLineCommand)\nregisterCommand(addDraftLineCommand)\nregisterCommand(removeDraftLineCommand)\nregisterCommand(wipeTestInvoicesCommand)\n\nexport {\n postInvoiceCommand,\n editDraftLineCommand,\n addDraftLineCommand,\n removeDraftLineCommand,\n wipeTestInvoicesCommand,\n}\n\n/**\n * `billing.invoices.send` \u2014 emails a posted document with its frozen PDF\n * (Spec B). Every attempt is logged in `metadata.deliveries`; a transport\n * failure returns `status: 'failed'` instead of throwing. Not undoable: an\n * email cannot be recalled.\n */\nconst sendInvoiceCommand: CommandHandler<BillingInvoiceSendInput, SendInvoiceResult> = {\n id: 'billing.invoices.send',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoiceSendSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n return sendInvoice(getEm(ctx), { ...parsed, trigger: 'manual' })\n },\n}\n\nregisterCommand(sendInvoiceCommand)\n\nexport { sendInvoiceCommand }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -11,11 +11,17 @@ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
|
11
11
|
import { BillingAccount, BillingItem } from "../data/entities.js";
|
|
12
12
|
import { billingEntityIds } from "../data/entityIds.js";
|
|
13
13
|
import { findBySourceRef } from "../lib/idempotency.js";
|
|
14
|
+
import { getBillingSettings, taxRateColumn } from "../lib/billingSettings.js";
|
|
15
|
+
import { planPriceChange } from "../lib/itemPriceChange.js";
|
|
14
16
|
import {
|
|
15
17
|
billingItemBulkCreateSchema,
|
|
18
|
+
billingItemChangePriceSchema,
|
|
16
19
|
billingItemCreateSchema,
|
|
17
20
|
billingItemDeleteSchema,
|
|
18
21
|
billingItemUpdateSchema,
|
|
22
|
+
CHARGE_TIMING_RECURRING_ONLY_MESSAGE,
|
|
23
|
+
GROSS_TIERED_UNSUPPORTED_MESSAGE,
|
|
24
|
+
isTieredRate,
|
|
19
25
|
validateRateJson
|
|
20
26
|
} from "../data/validators.js";
|
|
21
27
|
const itemIndexer = { entityType: billingEntityIds.item };
|
|
@@ -57,6 +63,7 @@ const createItemCommand = {
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
validateRateJson(parsed.type, parsed.rateJson);
|
|
66
|
+
const taxRate = parsed.taxRate !== void 0 ? taxRateColumn(parsed.taxRate) : (await getBillingSettings(em, parsed)).defaultTaxRate;
|
|
60
67
|
const now = /* @__PURE__ */ new Date();
|
|
61
68
|
const entity = em.create(BillingItem, {
|
|
62
69
|
organizationId: parsed.organizationId,
|
|
@@ -71,6 +78,11 @@ const createItemCommand = {
|
|
|
71
78
|
subscriptionId: parsed.subscriptionId ?? null,
|
|
72
79
|
subscriptionItemId: parsed.subscriptionItemId ?? null,
|
|
73
80
|
sourceRef: parsed.sourceRef ?? null,
|
|
81
|
+
// Spec A defaults: bill in advance, prorate partial periods.
|
|
82
|
+
chargeTiming: parsed.chargeTiming ?? "in_advance",
|
|
83
|
+
prorate: parsed.prorate ?? true,
|
|
84
|
+
taxRate,
|
|
85
|
+
priceBasis: parsed.priceBasis ?? "net",
|
|
74
86
|
// `currency_mismatch` is set by Phase 2 / connectors when the
|
|
75
87
|
// posted value's currency differs from the account's. Stays
|
|
76
88
|
// false on create (operator opts in to flagging).
|
|
@@ -128,6 +140,25 @@ const updateItemCommand = {
|
|
|
128
140
|
record.subscriptionItemId = parsed.subscriptionItemId;
|
|
129
141
|
}
|
|
130
142
|
if (parsed.isActive !== void 0) record.isActive = parsed.isActive;
|
|
143
|
+
if (parsed.taxRate !== void 0) record.taxRate = taxRateColumn(parsed.taxRate);
|
|
144
|
+
if (parsed.chargeTiming !== void 0 || parsed.prorate !== void 0) {
|
|
145
|
+
const nonDefault = parsed.chargeTiming !== void 0 && parsed.chargeTiming !== "in_advance" || parsed.prorate === false;
|
|
146
|
+
if (record.type !== "recurring" && nonDefault) {
|
|
147
|
+
throw new CrudHttpError(400, {
|
|
148
|
+
error: CHARGE_TIMING_RECURRING_ONLY_MESSAGE,
|
|
149
|
+
code: "billing.item.charge_timing_recurring_only"
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (parsed.chargeTiming !== void 0) record.chargeTiming = parsed.chargeTiming;
|
|
153
|
+
if (parsed.prorate !== void 0) record.prorate = parsed.prorate;
|
|
154
|
+
}
|
|
155
|
+
if (parsed.priceBasis !== void 0) record.priceBasis = parsed.priceBasis;
|
|
156
|
+
if (record.priceBasis === "gross" && isTieredRate(record.rateJson)) {
|
|
157
|
+
throw new CrudHttpError(422, {
|
|
158
|
+
error: "Tiered usage rates are always entered net",
|
|
159
|
+
code: GROSS_TIERED_UNSUPPORTED_MESSAGE
|
|
160
|
+
});
|
|
161
|
+
}
|
|
131
162
|
record.updatedAt = /* @__PURE__ */ new Date();
|
|
132
163
|
await em.flush();
|
|
133
164
|
await emitCrudSideEffects({
|
|
@@ -223,6 +254,7 @@ const bulkCreateItemsCommand = {
|
|
|
223
254
|
const createdEntities = [];
|
|
224
255
|
const deferredIdLinks = [];
|
|
225
256
|
const seenInBatch = /* @__PURE__ */ new Set();
|
|
257
|
+
const defaultTaxRate = parsed.items.some((entry) => entry.taxRate === void 0) ? (await getBillingSettings(em, parsed)).defaultTaxRate : "0";
|
|
226
258
|
for (const entry of parsed.items) {
|
|
227
259
|
const sourceKey = entry.sourceRef ? `${entry.billAccountId}::${entry.sourceRef}` : null;
|
|
228
260
|
if (sourceKey) {
|
|
@@ -260,6 +292,10 @@ const bulkCreateItemsCommand = {
|
|
|
260
292
|
subscriptionId: entry.subscriptionId ?? null,
|
|
261
293
|
subscriptionItemId: entry.subscriptionItemId ?? null,
|
|
262
294
|
sourceRef: entry.sourceRef ?? null,
|
|
295
|
+
chargeTiming: entry.chargeTiming ?? "in_advance",
|
|
296
|
+
prorate: entry.prorate ?? true,
|
|
297
|
+
taxRate: entry.taxRate !== void 0 ? taxRateColumn(entry.taxRate) : defaultTaxRate,
|
|
298
|
+
priceBasis: entry.priceBasis ?? "net",
|
|
263
299
|
currencyMismatch: false,
|
|
264
300
|
billedToDate: null,
|
|
265
301
|
isActive: true,
|
|
@@ -304,12 +340,84 @@ const bulkCreateItemsCommand = {
|
|
|
304
340
|
};
|
|
305
341
|
}
|
|
306
342
|
};
|
|
343
|
+
const changeItemPriceCommand = {
|
|
344
|
+
id: "billing.items.change_price",
|
|
345
|
+
async execute(rawInput, ctx) {
|
|
346
|
+
const parsed = billingItemChangePriceSchema.parse(rawInput);
|
|
347
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
348
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
349
|
+
const em = getEm(ctx);
|
|
350
|
+
const { previous, created } = await em.transactional(async (tem) => {
|
|
351
|
+
const record = await tem.findOne(BillingItem, {
|
|
352
|
+
id: parsed.id,
|
|
353
|
+
tenantId: parsed.tenantId,
|
|
354
|
+
organizationId: parsed.organizationId,
|
|
355
|
+
deletedAt: null
|
|
356
|
+
});
|
|
357
|
+
if (!record) throw new CrudHttpError(404, { error: "Billing item not found" });
|
|
358
|
+
const planned = planPriceChange(record, parsed.effectiveDate);
|
|
359
|
+
if (!planned.ok) {
|
|
360
|
+
throw new CrudHttpError(422, { error: "The price cannot be changed on this date", code: planned.error });
|
|
361
|
+
}
|
|
362
|
+
const { plan } = planned;
|
|
363
|
+
const now = /* @__PURE__ */ new Date();
|
|
364
|
+
record.billEndDate = plan.oldItemEndDate;
|
|
365
|
+
record.updatedAt = now;
|
|
366
|
+
const copy = tem.create(BillingItem, {
|
|
367
|
+
organizationId: record.organizationId,
|
|
368
|
+
tenantId: record.tenantId,
|
|
369
|
+
billAccountId: record.billAccountId,
|
|
370
|
+
type: record.type,
|
|
371
|
+
billStartDate: plan.newItemStartDate,
|
|
372
|
+
billEndDate: plan.newItemEndDate,
|
|
373
|
+
description: record.description,
|
|
374
|
+
rateJson: { ...record.rateJson ?? {}, unit_price: parsed.unitPrice },
|
|
375
|
+
uomCode: record.uomCode ?? null,
|
|
376
|
+
subscriptionId: record.subscriptionId ?? null,
|
|
377
|
+
subscriptionItemId: record.subscriptionItemId ?? null,
|
|
378
|
+
sourceRef: null,
|
|
379
|
+
chargeTiming: record.chargeTiming,
|
|
380
|
+
prorate: record.prorate,
|
|
381
|
+
taxRate: record.taxRate,
|
|
382
|
+
priceBasis: record.priceBasis,
|
|
383
|
+
currencyMismatch: record.currencyMismatch,
|
|
384
|
+
billedToDate: null,
|
|
385
|
+
isActive: true,
|
|
386
|
+
createdAt: now,
|
|
387
|
+
updatedAt: now
|
|
388
|
+
});
|
|
389
|
+
tem.persist(copy);
|
|
390
|
+
await tem.flush();
|
|
391
|
+
return { previous: record, created: copy };
|
|
392
|
+
});
|
|
393
|
+
const dataEngine = getDataEngine(ctx);
|
|
394
|
+
await emitCrudSideEffects({
|
|
395
|
+
dataEngine,
|
|
396
|
+
action: "updated",
|
|
397
|
+
entity: previous,
|
|
398
|
+
identifiers: { id: previous.id, tenantId: previous.tenantId, organizationId: previous.organizationId },
|
|
399
|
+
indexer: itemIndexer,
|
|
400
|
+
events: itemEvents
|
|
401
|
+
});
|
|
402
|
+
await emitCrudSideEffects({
|
|
403
|
+
dataEngine,
|
|
404
|
+
action: "created",
|
|
405
|
+
entity: created,
|
|
406
|
+
identifiers: { id: created.id, tenantId: created.tenantId, organizationId: created.organizationId },
|
|
407
|
+
indexer: itemIndexer,
|
|
408
|
+
events: itemEvents
|
|
409
|
+
});
|
|
410
|
+
return { previousItemId: previous.id, newItemId: created.id };
|
|
411
|
+
}
|
|
412
|
+
};
|
|
307
413
|
registerCommand(createItemCommand);
|
|
308
414
|
registerCommand(updateItemCommand);
|
|
309
415
|
registerCommand(deleteItemCommand);
|
|
310
416
|
registerCommand(bulkCreateItemsCommand);
|
|
417
|
+
registerCommand(changeItemPriceCommand);
|
|
311
418
|
export {
|
|
312
419
|
bulkCreateItemsCommand,
|
|
420
|
+
changeItemPriceCommand,
|
|
313
421
|
createItemCommand,
|
|
314
422
|
deleteItemCommand,
|
|
315
423
|
updateItemCommand
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/commands/items.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 {\n emitCrudSideEffects,\n requireId,\n} 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, BillingItem } from '../data/entities'\nimport { billingEntityIds } from '../data/entityIds'\nimport { findBySourceRef } from '../lib/idempotency'\nimport {\n billingItemBulkCreateSchema,\n billingItemCreateSchema,\n billingItemDeleteSchema,\n billingItemUpdateSchema,\n validateRateJson,\n type BillingItemBulkCreateInput,\n type BillingItemCreateInput,\n type BillingItemDeleteInput,\n type BillingItemUpdateInput,\n} from '../data/validators'\n\n/**\n * BillingItem writes \u2014 create / update / soft-delete.\n *\n * Create is idempotent via `source_ref`: if the integrator POSTs the\n * same `(tenant_id, bill_account_id, source_ref)` twice, the second\n * call returns the existing item with `deduplicated: true`. The\n * underlying unique index is the correctness gate; this pre-check keeps\n * the happy path single-round-trip and the response body informative.\n */\n\nconst itemIndexer = { entityType: billingEntityIds.item } as const\nconst itemEvents = { module: 'billing', entity: 'item', 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\nasync function assertAccountInScope(\n em: EntityManager,\n params: { tenantId: string; organizationId: string; billAccountId: string },\n): Promise<BillingAccount> {\n const account = await em.findOne(BillingAccount, {\n id: params.billAccountId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n deletedAt: null,\n })\n if (!account) {\n throw new CrudHttpError(404, { error: 'Billing account not found' })\n }\n return account\n}\n\n// \u2500\u2500\u2500 Create (idempotent) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 createItemCommand: CommandHandler<\n BillingItemCreateInput,\n { id: string; deduplicated: boolean }\n> = {\n id: 'billing.items.create',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const account = await assertAccountInScope(em, parsed)\n\n // Idempotency: if `source_ref` provided and matches an existing\n // (non-deleted) row, return that row. The unique index is the\n // final correctness gate \u2014 this is the happy-path early return.\n if (parsed.sourceRef) {\n const existing = await findBySourceRef(em, BillingItem, {\n tenantId: parsed.tenantId,\n billAccountId: parsed.billAccountId,\n sourceRef: parsed.sourceRef,\n })\n if (existing) {\n return { id: existing.id, deduplicated: true }\n }\n }\n\n // Re-validate the rate_json shape against the resolved type to\n // future-proof against schema drift between the API surface and\n // the persistence layer.\n validateRateJson(parsed.type, parsed.rateJson)\n\n const now = new Date()\n const entity = em.create(BillingItem, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: account.id,\n type: parsed.type,\n billStartDate: parsed.billStartDate,\n billEndDate: parsed.billEndDate ?? null,\n description: parsed.description,\n rateJson: parsed.rateJson as Record<string, unknown>,\n uomCode: parsed.uomCode ?? null,\n subscriptionId: parsed.subscriptionId ?? null,\n subscriptionItemId: parsed.subscriptionItemId ?? null,\n sourceRef: parsed.sourceRef ?? null,\n // `currency_mismatch` is set by Phase 2 / connectors when the\n // posted value's currency differs from the account's. Stays\n // false on create (operator opts in to flagging).\n currencyMismatch: false,\n // `billed_to_date` is set by the Bill Run engine on the first\n // real-run that emits this item. Always null on create.\n billedToDate: null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entity)\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'created',\n entity,\n identifiers: {\n id: entity.id,\n tenantId: entity.tenantId,\n organizationId: entity.organizationId,\n },\n indexer: itemIndexer,\n events: itemEvents,\n })\n\n return { id: entity.id, deduplicated: false }\n },\n}\n\n// \u2500\u2500\u2500 Update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 updateItemCommand: CommandHandler<BillingItemUpdateInput, { id: string }> = {\n id: 'billing.items.update',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemUpdateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const record = await em.findOne(BillingItem, {\n id: parsed.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!record) {\n throw new CrudHttpError(404, { error: 'Billing item not found' })\n }\n\n if (parsed.billStartDate !== undefined) record.billStartDate = parsed.billStartDate\n if (parsed.billEndDate !== undefined) record.billEndDate = parsed.billEndDate\n if (parsed.description !== undefined) record.description = parsed.description\n if (parsed.rateJson !== undefined) {\n // Re-validate against the current (immutable) type before persist.\n validateRateJson(record.type as 'one_time' | 'recurring' | 'usage', parsed.rateJson)\n record.rateJson = parsed.rateJson as Record<string, unknown>\n }\n if (parsed.uomCode !== undefined) record.uomCode = parsed.uomCode\n if (parsed.subscriptionId !== undefined) record.subscriptionId = parsed.subscriptionId\n if (parsed.subscriptionItemId !== undefined) {\n record.subscriptionItemId = parsed.subscriptionItemId\n }\n if (parsed.isActive !== undefined) record.isActive = parsed.isActive\n\n record.updatedAt = new Date()\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n },\n indexer: itemIndexer,\n events: itemEvents,\n })\n\n return { id: record.id }\n },\n}\n\n// \u2500\u2500\u2500 Delete (soft) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 deleteItemCommand: CommandHandler<BillingItemDeleteInput, { id: string }> = {\n id: 'billing.items.delete',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemDeleteSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const id = requireId(parsed)\n\n const em = getEm(ctx)\n const record = await em.findOne(BillingItem, {\n id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!record) {\n throw new CrudHttpError(404, { error: 'Billing item not found' })\n }\n\n record.deletedAt = new Date()\n record.updatedAt = record.deletedAt\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n },\n indexer: itemIndexer,\n events: itemEvents,\n })\n\n return { id: record.id }\n },\n}\n\n// \u2500\u2500\u2500 Bulk create (idempotent, batched) \u2500\u2500\u2500\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// Collapses N `billing.items.create` round-trips into one command.\n// The CPQ connector activating a large subscription is the primary\n// caller \u2014 it used to fire one create per charge.\n//\n// Idempotency is still per-`source_ref` but the existence check is a\n// SINGLE query over all source_refs in the batch, instead of one\n// `findOne` per item. Inserts are one `em.flush()` for the whole\n// batch. So a 200-charge activation goes from ~200 round-trips +\n// 200 flushes to ~3 queries + 1 flush.\n\ntype BulkCreateResultEntry = {\n sourceRef: string | null\n id: string\n deduplicated: boolean\n}\n\nconst bulkCreateItemsCommand: CommandHandler<\n BillingItemBulkCreateInput,\n { created: number; deduplicated: number; items: BulkCreateResultEntry[] }\n> = {\n id: 'billing.items.bulk_create',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemBulkCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n\n // Validate every referenced account exists + is in scope. One\n // `$in` query for the distinct account ids, not one per item.\n const accountIds = Array.from(new Set(parsed.items.map((i) => i.billAccountId)))\n const accounts = await em.find(BillingAccount, {\n id: { $in: accountIds },\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n } as never)\n const knownAccountIds = new Set(accounts.map((a) => a.id))\n const missingAccount = accountIds.find((id) => !knownAccountIds.has(id))\n if (missingAccount) {\n throw new CrudHttpError(404, {\n error: 'Billing account not found',\n billAccountId: missingAccount,\n })\n }\n\n // Batched idempotency check \u2014 one query over every source_ref in\n // the batch. `source_ref` is unique per `(tenant, bill_account,\n // source_ref)`, so we key the existing-set on that triple.\n const sourceRefs = parsed.items\n .map((i) => i.sourceRef)\n .filter((ref): ref is string => typeof ref === 'string' && ref.length > 0)\n const existingBySourceKey = new Map<string, string>()\n if (sourceRefs.length > 0) {\n const existing = await em.find(BillingItem, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n billAccountId: { $in: accountIds },\n sourceRef: { $in: Array.from(new Set(sourceRefs)) },\n deletedAt: null,\n } as never)\n for (const row of existing) {\n if (row.sourceRef) {\n existingBySourceKey.set(`${row.billAccountId}::${row.sourceRef}`, row.id)\n }\n }\n }\n\n const now = new Date()\n const results: BulkCreateResultEntry[] = []\n const createdEntities: BillingItem[] = []\n // Result entries whose `id` is only known after the flush \u2014 the\n // BillingItem PK is a DB-generated UUID, unassigned until then.\n // Covers freshly-created rows and in-batch dedup rows that point\n // at a sibling created earlier in the same payload.\n const deferredIdLinks: Array<{ result: BulkCreateResultEntry; entity: BillingItem }> = []\n\n // Track source_refs created within THIS batch so a payload that\n // repeats the same source_ref twice dedups against itself (the\n // unique index would otherwise blow up the whole flush).\n const seenInBatch = new Set<string>()\n\n for (const entry of parsed.items) {\n const sourceKey = entry.sourceRef\n ? `${entry.billAccountId}::${entry.sourceRef}`\n : null\n\n if (sourceKey) {\n const existingId = existingBySourceKey.get(sourceKey)\n if (existingId) {\n results.push({ sourceRef: entry.sourceRef ?? null, id: existingId, deduplicated: true })\n continue\n }\n if (seenInBatch.has(sourceKey)) {\n // Duplicate source_ref inside the same payload \u2014 first wins,\n // the repeat is reported deduplicated against the first.\n const firstCreated = createdEntities.find(\n (e) => e.billAccountId === entry.billAccountId && e.sourceRef === entry.sourceRef,\n )\n const dedupResult: BulkCreateResultEntry = {\n sourceRef: entry.sourceRef ?? null,\n id: '',\n deduplicated: true,\n }\n results.push(dedupResult)\n if (firstCreated) deferredIdLinks.push({ result: dedupResult, entity: firstCreated })\n continue\n }\n seenInBatch.add(sourceKey)\n }\n\n validateRateJson(entry.type, entry.rateJson)\n const item = em.create(BillingItem, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: entry.billAccountId,\n type: entry.type,\n billStartDate: entry.billStartDate,\n billEndDate: entry.billEndDate ?? null,\n description: entry.description,\n rateJson: entry.rateJson as Record<string, unknown>,\n uomCode: entry.uomCode ?? null,\n subscriptionId: entry.subscriptionId ?? null,\n subscriptionItemId: entry.subscriptionItemId ?? null,\n sourceRef: entry.sourceRef ?? null,\n currencyMismatch: false,\n billedToDate: null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(item)\n createdEntities.push(item)\n const createdResult: BulkCreateResultEntry = {\n sourceRef: entry.sourceRef ?? null,\n id: '',\n deduplicated: false,\n }\n results.push(createdResult)\n deferredIdLinks.push({ result: createdResult, entity: item })\n }\n\n // Single flush for the whole batch.\n if (createdEntities.length > 0) {\n await em.flush()\n }\n\n // The BillingItem UUID PK is assigned by the flush \u2014 backfill it\n // into the result entries now that the value exists.\n for (const link of deferredIdLinks) {\n link.result.id = link.entity.id\n }\n\n // Side effects after the commit. Emitting per-entity keeps the\n // query-index + event contract identical to single-create \u2014 the\n // engine / search index can't tell a bulk insert from N singles.\n const dataEngine = getDataEngine(ctx)\n for (const item of createdEntities) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity: item,\n identifiers: {\n id: item.id,\n tenantId: item.tenantId,\n organizationId: item.organizationId,\n },\n indexer: itemIndexer,\n events: itemEvents,\n })\n }\n\n return {\n created: createdEntities.length,\n deduplicated: results.filter((r) => r.deduplicated).length,\n items: results,\n }\n },\n}\n\nregisterCommand(createItemCommand)\nregisterCommand(updateItemCommand)\nregisterCommand(deleteItemCommand)\nregisterCommand(bulkCreateItemsCommand)\n\nexport {\n createItemCommand,\n updateItemCommand,\n deleteItemCommand,\n bulkCreateItemsCommand,\n}\n"],
|
|
5
|
-
"mappings": "AAEA,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAE9B,SAAS,gBAAgB,mBAAmB;AAC5C,SAAS,wBAAwB;AACjC,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AAYP,MAAM,cAAc,EAAE,YAAY,iBAAiB,KAAK;AACxD,MAAM,aAAa,EAAE,QAAQ,WAAW,QAAQ,QAAQ,YAAY,KAAK;AAEzE,SAAS,MAAM,KAA2C;AACxD,SAAQ,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC7D;AAEA,SAAS,cAAc,KAAwC;AAC7D,SAAO,IAAI,UAAU,QAAQ,YAAY;AAC3C;AAEA,eAAe,qBACb,IACA,QACyB;AACzB,QAAM,UAAU,MAAM,GAAG,QAAQ,gBAAgB;AAAA,IAC/C,IAAI,OAAO;AAAA,IACX,UAAU,OAAO;AAAA,IACjB,gBAAgB,OAAO;AAAA,IACvB,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,4BAA4B,CAAC;AAAA,EACrE;AACA,SAAO;AACT;AAIA,MAAM,oBAGF;AAAA,EACF,IAAI;AAAA,EAEJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,wBAAwB,MAAM,QAAQ;AACrD,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAElD,UAAM,KAAK,MAAM,GAAG;AACpB,UAAM,UAAU,MAAM,qBAAqB,IAAI,MAAM;AAKrD,QAAI,OAAO,WAAW;AACpB,YAAM,WAAW,MAAM,gBAAgB,IAAI,aAAa;AAAA,QACtD,UAAU,OAAO;AAAA,QACjB,eAAe,OAAO;AAAA,QACtB,WAAW,OAAO;AAAA,MACpB,CAAC;AACD,UAAI,UAAU;AACZ,eAAO,EAAE,IAAI,SAAS,IAAI,cAAc,KAAK;AAAA,MAC/C;AAAA,IACF;AAKA,qBAAiB,OAAO,MAAM,OAAO,QAAQ;AAE7C,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,SAAS,GAAG,OAAO,aAAa;AAAA,MACpC,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,eAAe,QAAQ;AAAA,MACvB,MAAM,OAAO;AAAA,MACb,eAAe,OAAO;AAAA,MACtB,aAAa,OAAO,eAAe;AAAA,MACnC,aAAa,OAAO;AAAA,MACpB,UAAU,OAAO;AAAA,MACjB,SAAS,OAAO,WAAW;AAAA,MAC3B,gBAAgB,OAAO,kBAAkB;AAAA,MACzC,oBAAoB,OAAO,sBAAsB;AAAA,MACjD,WAAW,OAAO,aAAa;AAAA;AAAA;AAAA;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 {\n emitCrudSideEffects,\n requireId,\n} 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, BillingItem } from '../data/entities'\nimport { billingEntityIds } from '../data/entityIds'\nimport { findBySourceRef } from '../lib/idempotency'\nimport { getBillingSettings, taxRateColumn } from '../lib/billingSettings'\nimport { planPriceChange } from '../lib/itemPriceChange'\nimport {\n billingItemBulkCreateSchema,\n billingItemChangePriceSchema,\n type BillingItemChangePriceInput,\n billingItemCreateSchema,\n billingItemDeleteSchema,\n billingItemUpdateSchema,\n CHARGE_TIMING_RECURRING_ONLY_MESSAGE,\n GROSS_TIERED_UNSUPPORTED_MESSAGE,\n isTieredRate,\n validateRateJson,\n type BillingItemBulkCreateInput,\n type BillingItemCreateInput,\n type BillingItemDeleteInput,\n type BillingItemUpdateInput,\n} from '../data/validators'\n\n/**\n * BillingItem writes \u2014 create / update / soft-delete.\n *\n * Create is idempotent via `source_ref`: if the integrator POSTs the\n * same `(tenant_id, bill_account_id, source_ref)` twice, the second\n * call returns the existing item with `deduplicated: true`. The\n * underlying unique index is the correctness gate; this pre-check keeps\n * the happy path single-round-trip and the response body informative.\n */\n\nconst itemIndexer = { entityType: billingEntityIds.item } as const\nconst itemEvents = { module: 'billing', entity: 'item', 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\nasync function assertAccountInScope(\n em: EntityManager,\n params: { tenantId: string; organizationId: string; billAccountId: string },\n): Promise<BillingAccount> {\n const account = await em.findOne(BillingAccount, {\n id: params.billAccountId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n deletedAt: null,\n })\n if (!account) {\n throw new CrudHttpError(404, { error: 'Billing account not found' })\n }\n return account\n}\n\n// \u2500\u2500\u2500 Create (idempotent) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 createItemCommand: CommandHandler<\n BillingItemCreateInput,\n { id: string; deduplicated: boolean }\n> = {\n id: 'billing.items.create',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const account = await assertAccountInScope(em, parsed)\n\n // Idempotency: if `source_ref` provided and matches an existing\n // (non-deleted) row, return that row. The unique index is the\n // final correctness gate \u2014 this is the happy-path early return.\n if (parsed.sourceRef) {\n const existing = await findBySourceRef(em, BillingItem, {\n tenantId: parsed.tenantId,\n billAccountId: parsed.billAccountId,\n sourceRef: parsed.sourceRef,\n })\n if (existing) {\n return { id: existing.id, deduplicated: true }\n }\n }\n\n // Re-validate the rate_json shape against the resolved type to\n // future-proof against schema drift between the API surface and\n // the persistence layer.\n validateRateJson(parsed.type, parsed.rateJson)\n\n // Spec B (B-D1): an omitted rate takes the org default, stored on the item.\n const taxRate =\n parsed.taxRate !== undefined\n ? taxRateColumn(parsed.taxRate)\n : (await getBillingSettings(em, parsed)).defaultTaxRate\n\n const now = new Date()\n const entity = em.create(BillingItem, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: account.id,\n type: parsed.type,\n billStartDate: parsed.billStartDate,\n billEndDate: parsed.billEndDate ?? null,\n description: parsed.description,\n rateJson: parsed.rateJson as Record<string, unknown>,\n uomCode: parsed.uomCode ?? null,\n subscriptionId: parsed.subscriptionId ?? null,\n subscriptionItemId: parsed.subscriptionItemId ?? null,\n sourceRef: parsed.sourceRef ?? null,\n // Spec A defaults: bill in advance, prorate partial periods.\n chargeTiming: parsed.chargeTiming ?? 'in_advance',\n prorate: parsed.prorate ?? true,\n taxRate,\n priceBasis: parsed.priceBasis ?? 'net',\n // `currency_mismatch` is set by Phase 2 / connectors when the\n // posted value's currency differs from the account's. Stays\n // false on create (operator opts in to flagging).\n currencyMismatch: false,\n // `billed_to_date` is set by the Bill Run engine on the first\n // real-run that emits this item. Always null on create.\n billedToDate: null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entity)\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'created',\n entity,\n identifiers: {\n id: entity.id,\n tenantId: entity.tenantId,\n organizationId: entity.organizationId,\n },\n indexer: itemIndexer,\n events: itemEvents,\n })\n\n return { id: entity.id, deduplicated: false }\n },\n}\n\n// \u2500\u2500\u2500 Update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 updateItemCommand: CommandHandler<BillingItemUpdateInput, { id: string }> = {\n id: 'billing.items.update',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemUpdateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const record = await em.findOne(BillingItem, {\n id: parsed.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!record) {\n throw new CrudHttpError(404, { error: 'Billing item not found' })\n }\n\n if (parsed.billStartDate !== undefined) record.billStartDate = parsed.billStartDate\n if (parsed.billEndDate !== undefined) record.billEndDate = parsed.billEndDate\n if (parsed.description !== undefined) record.description = parsed.description\n if (parsed.rateJson !== undefined) {\n // Re-validate against the current (immutable) type before persist.\n validateRateJson(record.type as 'one_time' | 'recurring' | 'usage', parsed.rateJson)\n record.rateJson = parsed.rateJson as Record<string, unknown>\n }\n if (parsed.uomCode !== undefined) record.uomCode = parsed.uomCode\n if (parsed.subscriptionId !== undefined) record.subscriptionId = parsed.subscriptionId\n if (parsed.subscriptionItemId !== undefined) {\n record.subscriptionItemId = parsed.subscriptionItemId\n }\n if (parsed.isActive !== undefined) record.isActive = parsed.isActive\n if (parsed.taxRate !== undefined) record.taxRate = taxRateColumn(parsed.taxRate)\n if (parsed.chargeTiming !== undefined || parsed.prorate !== undefined) {\n // Timing / proration only apply to recurring items (validators can't\n // check this on update \u2014 the item type isn't in the payload).\n const nonDefault =\n (parsed.chargeTiming !== undefined && parsed.chargeTiming !== 'in_advance') ||\n parsed.prorate === false\n if (record.type !== 'recurring' && nonDefault) {\n throw new CrudHttpError(400, {\n error: CHARGE_TIMING_RECURRING_ONLY_MESSAGE,\n code: 'billing.item.charge_timing_recurring_only',\n })\n }\n if (parsed.chargeTiming !== undefined) record.chargeTiming = parsed.chargeTiming\n if (parsed.prorate !== undefined) record.prorate = parsed.prorate\n }\n\n if (parsed.priceBasis !== undefined) record.priceBasis = parsed.priceBasis\n // Spec F (P-D4): tiered usage rates are always net \u2014 checked on the\n // resulting state, since either field may change alone.\n if (record.priceBasis === 'gross' && isTieredRate(record.rateJson)) {\n throw new CrudHttpError(422, {\n error: 'Tiered usage rates are always entered net',\n code: GROSS_TIERED_UNSUPPORTED_MESSAGE,\n })\n }\n\n record.updatedAt = new Date()\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n },\n indexer: itemIndexer,\n events: itemEvents,\n })\n\n return { id: record.id }\n },\n}\n\n// \u2500\u2500\u2500 Delete (soft) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 deleteItemCommand: CommandHandler<BillingItemDeleteInput, { id: string }> = {\n id: 'billing.items.delete',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemDeleteSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const id = requireId(parsed)\n\n const em = getEm(ctx)\n const record = await em.findOne(BillingItem, {\n id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!record) {\n throw new CrudHttpError(404, { error: 'Billing item not found' })\n }\n\n record.deletedAt = new Date()\n record.updatedAt = record.deletedAt\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n },\n indexer: itemIndexer,\n events: itemEvents,\n })\n\n return { id: record.id }\n },\n}\n\n// \u2500\u2500\u2500 Bulk create (idempotent, batched) \u2500\u2500\u2500\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// Collapses N `billing.items.create` round-trips into one command.\n// The CPQ connector activating a large subscription is the primary\n// caller \u2014 it used to fire one create per charge.\n//\n// Idempotency is still per-`source_ref` but the existence check is a\n// SINGLE query over all source_refs in the batch, instead of one\n// `findOne` per item. Inserts are one `em.flush()` for the whole\n// batch. So a 200-charge activation goes from ~200 round-trips +\n// 200 flushes to ~3 queries + 1 flush.\n\ntype BulkCreateResultEntry = {\n sourceRef: string | null\n id: string\n deduplicated: boolean\n}\n\nconst bulkCreateItemsCommand: CommandHandler<\n BillingItemBulkCreateInput,\n { created: number; deduplicated: number; items: BulkCreateResultEntry[] }\n> = {\n id: 'billing.items.bulk_create',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemBulkCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n\n // Validate every referenced account exists + is in scope. One\n // `$in` query for the distinct account ids, not one per item.\n const accountIds = Array.from(new Set(parsed.items.map((i) => i.billAccountId)))\n const accounts = await em.find(BillingAccount, {\n id: { $in: accountIds },\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n } as never)\n const knownAccountIds = new Set(accounts.map((a) => a.id))\n const missingAccount = accountIds.find((id) => !knownAccountIds.has(id))\n if (missingAccount) {\n throw new CrudHttpError(404, {\n error: 'Billing account not found',\n billAccountId: missingAccount,\n })\n }\n\n // Batched idempotency check \u2014 one query over every source_ref in\n // the batch. `source_ref` is unique per `(tenant, bill_account,\n // source_ref)`, so we key the existing-set on that triple.\n const sourceRefs = parsed.items\n .map((i) => i.sourceRef)\n .filter((ref): ref is string => typeof ref === 'string' && ref.length > 0)\n const existingBySourceKey = new Map<string, string>()\n if (sourceRefs.length > 0) {\n const existing = await em.find(BillingItem, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n billAccountId: { $in: accountIds },\n sourceRef: { $in: Array.from(new Set(sourceRefs)) },\n deletedAt: null,\n } as never)\n for (const row of existing) {\n if (row.sourceRef) {\n existingBySourceKey.set(`${row.billAccountId}::${row.sourceRef}`, row.id)\n }\n }\n }\n\n const now = new Date()\n const results: BulkCreateResultEntry[] = []\n const createdEntities: BillingItem[] = []\n // Result entries whose `id` is only known after the flush \u2014 the\n // BillingItem PK is a DB-generated UUID, unassigned until then.\n // Covers freshly-created rows and in-batch dedup rows that point\n // at a sibling created earlier in the same payload.\n const deferredIdLinks: Array<{ result: BulkCreateResultEntry; entity: BillingItem }> = []\n\n // Track source_refs created within THIS batch so a payload that\n // repeats the same source_ref twice dedups against itself (the\n // unique index would otherwise blow up the whole flush).\n const seenInBatch = new Set<string>()\n\n // Spec B (B-D1): org default rate, read once and only when an entry omits it.\n const defaultTaxRate = parsed.items.some((entry) => entry.taxRate === undefined)\n ? (await getBillingSettings(em, parsed)).defaultTaxRate\n : '0'\n\n for (const entry of parsed.items) {\n const sourceKey = entry.sourceRef\n ? `${entry.billAccountId}::${entry.sourceRef}`\n : null\n\n if (sourceKey) {\n const existingId = existingBySourceKey.get(sourceKey)\n if (existingId) {\n results.push({ sourceRef: entry.sourceRef ?? null, id: existingId, deduplicated: true })\n continue\n }\n if (seenInBatch.has(sourceKey)) {\n // Duplicate source_ref inside the same payload \u2014 first wins,\n // the repeat is reported deduplicated against the first.\n const firstCreated = createdEntities.find(\n (e) => e.billAccountId === entry.billAccountId && e.sourceRef === entry.sourceRef,\n )\n const dedupResult: BulkCreateResultEntry = {\n sourceRef: entry.sourceRef ?? null,\n id: '',\n deduplicated: true,\n }\n results.push(dedupResult)\n if (firstCreated) deferredIdLinks.push({ result: dedupResult, entity: firstCreated })\n continue\n }\n seenInBatch.add(sourceKey)\n }\n\n validateRateJson(entry.type, entry.rateJson)\n const item = em.create(BillingItem, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: entry.billAccountId,\n type: entry.type,\n billStartDate: entry.billStartDate,\n billEndDate: entry.billEndDate ?? null,\n description: entry.description,\n rateJson: entry.rateJson as Record<string, unknown>,\n uomCode: entry.uomCode ?? null,\n subscriptionId: entry.subscriptionId ?? null,\n subscriptionItemId: entry.subscriptionItemId ?? null,\n sourceRef: entry.sourceRef ?? null,\n chargeTiming: entry.chargeTiming ?? 'in_advance',\n prorate: entry.prorate ?? true,\n taxRate: entry.taxRate !== undefined ? taxRateColumn(entry.taxRate) : defaultTaxRate,\n priceBasis: entry.priceBasis ?? 'net',\n currencyMismatch: false,\n billedToDate: null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(item)\n createdEntities.push(item)\n const createdResult: BulkCreateResultEntry = {\n sourceRef: entry.sourceRef ?? null,\n id: '',\n deduplicated: false,\n }\n results.push(createdResult)\n deferredIdLinks.push({ result: createdResult, entity: item })\n }\n\n // Single flush for the whole batch.\n if (createdEntities.length > 0) {\n await em.flush()\n }\n\n // The BillingItem UUID PK is assigned by the flush \u2014 backfill it\n // into the result entries now that the value exists.\n for (const link of deferredIdLinks) {\n link.result.id = link.entity.id\n }\n\n // Side effects after the commit. Emitting per-entity keeps the\n // query-index + event contract identical to single-create \u2014 the\n // engine / search index can't tell a bulk insert from N singles.\n const dataEngine = getDataEngine(ctx)\n for (const item of createdEntities) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity: item,\n identifiers: {\n id: item.id,\n tenantId: item.tenantId,\n organizationId: item.organizationId,\n },\n indexer: itemIndexer,\n events: itemEvents,\n })\n }\n\n return {\n created: createdEntities.length,\n deduplicated: results.filter((r) => r.deduplicated).length,\n items: results,\n }\n },\n}\n\n// \u2500\u2500\u2500 Change price (recurring) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Ends the item the day before `effectiveDate` and creates a copy with the\n// new unit price from `effectiveDate` on \u2014 one transaction, so the account\n// never has both items or neither. The copy keeps everything else (account,\n// description, timing, proration, VAT rate, price basis, references, end\n// date). No `source_ref` on the copy: it is the idempotency key of the\n// integration that created the original. Not undoable (like the run\n// commands): reverse it by editing the two items.\n\nconst changeItemPriceCommand: CommandHandler<\n BillingItemChangePriceInput,\n { previousItemId: string; newItemId: string }\n> = {\n id: 'billing.items.change_price',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemChangePriceSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const { previous, created } = await em.transactional(async (tem) => {\n const record = await tem.findOne(BillingItem, {\n id: parsed.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!record) throw new CrudHttpError(404, { error: 'Billing item not found' })\n const planned = planPriceChange(record, parsed.effectiveDate)\n if (!planned.ok) {\n throw new CrudHttpError(422, { error: 'The price cannot be changed on this date', code: planned.error })\n }\n const { plan } = planned\n const now = new Date()\n record.billEndDate = plan.oldItemEndDate\n record.updatedAt = now\n const copy = tem.create(BillingItem, {\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n billAccountId: record.billAccountId,\n type: record.type,\n billStartDate: plan.newItemStartDate,\n billEndDate: plan.newItemEndDate,\n description: record.description,\n rateJson: { ...(record.rateJson ?? {}), unit_price: parsed.unitPrice },\n uomCode: record.uomCode ?? null,\n subscriptionId: record.subscriptionId ?? null,\n subscriptionItemId: record.subscriptionItemId ?? null,\n sourceRef: null,\n chargeTiming: record.chargeTiming,\n prorate: record.prorate,\n taxRate: record.taxRate,\n priceBasis: record.priceBasis,\n currencyMismatch: record.currencyMismatch,\n billedToDate: null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n tem.persist(copy)\n await tem.flush()\n return { previous: record, created: copy }\n })\n\n // Side effects after commit.\n const dataEngine = getDataEngine(ctx)\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: previous,\n identifiers: { id: previous.id, tenantId: previous.tenantId, organizationId: previous.organizationId },\n indexer: itemIndexer,\n events: itemEvents,\n })\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity: created,\n identifiers: { id: created.id, tenantId: created.tenantId, organizationId: created.organizationId },\n indexer: itemIndexer,\n events: itemEvents,\n })\n return { previousItemId: previous.id, newItemId: created.id }\n },\n}\n\nregisterCommand(createItemCommand)\nregisterCommand(updateItemCommand)\nregisterCommand(deleteItemCommand)\nregisterCommand(bulkCreateItemsCommand)\nregisterCommand(changeItemPriceCommand)\n\nexport {\n createItemCommand,\n updateItemCommand,\n deleteItemCommand,\n bulkCreateItemsCommand,\n changeItemPriceCommand,\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|