@dainamite/billing 0.19.0 → 0.20.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 +82 -0
- package/README.md +14 -0
- package/dist/modules/billing/api/export/account/[id]/route.js +5 -5
- package/dist/modules/billing/api/export/account/[id]/route.js.map +1 -1
- package/dist/modules/billing/api/invoices/add-line/route.js +1 -1
- package/dist/modules/billing/api/invoices/add-line/route.js.map +1 -1
- package/dist/modules/billing/api/invoices/edit-line/route.js +1 -1
- package/dist/modules/billing/api/invoices/edit-line/route.js.map +1 -1
- package/dist/modules/billing/api/invoices/remove-line/route.js +1 -1
- package/dist/modules/billing/api/invoices/remove-line/route.js.map +1 -1
- package/dist/modules/billing/api/run-outcomes/route.js +2 -2
- package/dist/modules/billing/api/run-outcomes/route.js.map +2 -2
- package/dist/modules/billing/api/runs/retry-failed/route.js +2 -2
- package/dist/modules/billing/api/runs/retry-failed/route.js.map +2 -2
- package/dist/modules/billing/api/runs/route.js +2 -2
- package/dist/modules/billing/api/runs/route.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/accounts/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/invoices/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/items/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/runs/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/statements/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/topups/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/transactions/page.js.map +1 -1
- package/dist/modules/billing/ce.js.map +2 -2
- package/dist/modules/billing/commands/invoices.js +4 -4
- package/dist/modules/billing/commands/invoices.js.map +2 -2
- package/dist/modules/billing/commands/runs.js +3 -3
- package/dist/modules/billing/commands/runs.js.map +2 -2
- package/dist/modules/billing/components/PrepaidPanel.js.map +1 -1
- package/dist/modules/billing/data/entities.js +56 -56
- package/dist/modules/billing/data/entities.js.map +2 -2
- package/dist/modules/billing/data/entityIds.js.map +2 -2
- package/dist/modules/billing/events.js +20 -0
- package/dist/modules/billing/events.js.map +2 -2
- package/dist/modules/billing/lib/billRunEngine.js +4 -4
- package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
- package/dist/modules/billing/lib/billRunLabel.js.map +1 -1
- package/dist/modules/billing/lib/prepaidPeriodClose.js.map +1 -1
- package/dist/modules/billing/lib/tenantLock.js.map +1 -1
- package/dist/modules/billing/workers/reap-stale-bill-runs.js +2 -2
- package/dist/modules/billing/workers/reap-stale-bill-runs.js.map +2 -2
- package/generated/entities/billing_account/index.ts +23 -0
- package/generated/entities/billing_account_balance/index.ts +13 -0
- package/generated/entities/billing_account_transaction/index.ts +21 -0
- package/generated/entities/billing_account_usage/index.ts +18 -0
- package/generated/entities/billing_draft_invoice_edit/index.ts +14 -0
- package/generated/entities/billing_item/index.ts +22 -0
- package/generated/entities/billing_run/index.ts +21 -0
- package/generated/entities/billing_run_outcome/index.ts +17 -0
- package/generated/entities/billing_statement/index.ts +21 -0
- package/generated/entities/billing_topup/index.ts +20 -0
- package/generated/entities.ids.generated.ts +28 -0
- package/package.json +11 -10
- package/src/modules/billing/__tests__/commands/invoices.test.ts +5 -5
- package/src/modules/billing/__tests__/entityRegistry.test.ts +319 -0
- package/src/modules/billing/__tests__/events.test.ts +142 -0
- package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +3 -3
- package/src/modules/billing/api/export/account/[id]/route.ts +7 -7
- package/src/modules/billing/api/invoices/add-line/route.ts +1 -1
- package/src/modules/billing/api/invoices/edit-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/remove-line/route.ts +1 -1
- package/src/modules/billing/api/run-outcomes/route.ts +2 -2
- package/src/modules/billing/api/runs/retry-failed/route.ts +2 -2
- package/src/modules/billing/api/runs/route.ts +2 -2
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +1 -1
- package/src/modules/billing/backend/billing/accounts/page.tsx +1 -1
- package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +1 -1
- package/src/modules/billing/backend/billing/invoices/page.tsx +1 -1
- package/src/modules/billing/backend/billing/items/page.tsx +1 -1
- package/src/modules/billing/backend/billing/runs/[id]/page.tsx +1 -1
- package/src/modules/billing/backend/billing/runs/page.tsx +1 -1
- package/src/modules/billing/backend/billing/statements/[id]/page.tsx +1 -1
- 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/ce.ts +8 -7
- package/src/modules/billing/commands/invoices.ts +9 -9
- package/src/modules/billing/commands/runs.ts +3 -3
- package/src/modules/billing/components/PrepaidPanel.tsx +1 -1
- package/src/modules/billing/data/entities.ts +31 -10
- package/src/modules/billing/data/entityIds.ts +16 -11
- package/src/modules/billing/events.ts +26 -0
- package/src/modules/billing/lib/billRunEngine.ts +12 -12
- package/src/modules/billing/lib/billRunLabel.ts +1 -1
- package/src/modules/billing/lib/prepaidPeriodClose.ts +3 -3
- package/src/modules/billing/lib/tenantLock.ts +1 -1
- package/src/modules/billing/workers/reap-stale-bill-runs.ts +4 -4
|
@@ -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, DraftInvoiceEdit } 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 `DraftInvoiceEdit` 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 DraftInvoiceEdit.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: DraftInvoiceEdit | 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(DraftInvoiceEdit, {\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: DraftInvoiceEdit | 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(DraftInvoiceEdit, {\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: DraftInvoiceEdit | 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(DraftInvoiceEdit, {\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": "AAEA,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAC9B,SAAS,cAAc,wBAAwB;AAC/C,SAAS,gBAAgB
|
|
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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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
ensureTenantScope
|
|
5
5
|
} from "@open-mercato/shared/lib/commands/scope";
|
|
6
6
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
7
|
-
import {
|
|
7
|
+
import { BillingRun, BillingRunOutcome } from "../data/entities.js";
|
|
8
8
|
import {
|
|
9
9
|
billingRunRetryFailedSchema,
|
|
10
10
|
billingRunTriggerSchema
|
|
@@ -63,7 +63,7 @@ const retryFailedCommand = {
|
|
|
63
63
|
ensureTenantScope(ctx, parsed.tenantId);
|
|
64
64
|
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
65
65
|
const em = getEm(ctx);
|
|
66
|
-
const parentRun = await em.findOne(
|
|
66
|
+
const parentRun = await em.findOne(BillingRun, {
|
|
67
67
|
id: parsed.billRunId,
|
|
68
68
|
tenantId: parsed.tenantId,
|
|
69
69
|
organizationId: parsed.organizationId,
|
|
@@ -72,7 +72,7 @@ const retryFailedCommand = {
|
|
|
72
72
|
if (!parentRun) {
|
|
73
73
|
throw new CrudHttpError(404, { error: "Parent Bill Run not found" });
|
|
74
74
|
}
|
|
75
|
-
const failedOutcomes = await em.find(
|
|
75
|
+
const failedOutcomes = await em.find(BillingRunOutcome, {
|
|
76
76
|
billRunId: parentRun.id,
|
|
77
77
|
tenantId: parsed.tenantId,
|
|
78
78
|
organizationId: parsed.organizationId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/commands/runs.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {
|
|
5
|
-
"mappings": "AAGA,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAC9B,SAAS,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { BillingRun, BillingRunOutcome } from '../data/entities'\nimport {\n billingRunRetryFailedSchema,\n billingRunTriggerSchema,\n type BillingRunRetryFailedInput,\n type BillingRunTriggerInput,\n} from '../data/validators'\nimport { runBillRun } from '../lib/billRunEngine'\nimport { TenantLockBusyError } from '../lib/tenantLock'\n\n/**\n * Bill Run trigger commands.\n *\n * Both commands wrap `runBillRun(...)` from `lib/billRunEngine`. The\n * lock-busy condition (concurrent run on the same tenant) is mapped to\n * HTTP 409 here so the API surface returns the canonical Conflict\n * response without each route knowing the lock primitive's\n * implementation.\n *\n * `triggeredBy` is intentionally NOT exposed on the input schema \u2014\n * client-driven triggers are always `'manual'`. The scheduler worker\n * (Phase 4) injects `'schedule'` directly through a separate\n * orchestrator path, not through this command.\n */\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction getContainer(ctx: CommandRuntimeContext): AwilixContainer {\n return ctx.container as AwilixContainer\n}\n\nfunction asLockBusyError(err: unknown): err is TenantLockBusyError {\n return err instanceof TenantLockBusyError\n}\n\n// \u2500\u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype TriggerResult = {\n billRunId: string\n status: string\n outcomesCount: number\n}\n\nconst triggerRunCommand: CommandHandler<BillingRunTriggerInput, TriggerResult> = {\n id: 'billing.runs.trigger',\n\n async execute(rawInput, ctx) {\n const parsed = billingRunTriggerSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const asOfDate = parsed.asOfDate ?? new Date()\n\n try {\n const { billRun, outcomes } = await runBillRun(em, getContainer(ctx), {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n asOfDate,\n triggeredBy: 'manual',\n mode: parsed.mode,\n scopedAccountIds: parsed.scopedAccountIds,\n catchUp: parsed.catchUp,\n })\n return {\n billRunId: billRun.id,\n status: billRun.status,\n outcomesCount: outcomes.length,\n }\n } catch (err) {\n if (asLockBusyError(err)) {\n // The route metadata maps `CrudHttpError(409)` to the canonical\n // 409 Conflict response. The error body carries the\n // `error_lock_busy` discriminator so clients can distinguish\n // \"lock busy\" from other 409 conditions.\n throw new CrudHttpError(409, {\n error: 'Another Bill Run is already in progress for this tenant',\n code: 'billing.run.lock_busy',\n tenantId: err.tenantId,\n lockName: err.lockName,\n })\n }\n throw err\n }\n },\n}\n\n// \u2500\u2500\u2500 Retry failed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst retryFailedCommand: CommandHandler<BillingRunRetryFailedInput, TriggerResult> = {\n id: 'billing.runs.retry_failed',\n\n async execute(rawInput, ctx) {\n const parsed = billingRunRetryFailedSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const parentRun = await em.findOne(BillingRun, {\n id: parsed.billRunId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!parentRun) {\n throw new CrudHttpError(404, { error: 'Parent Bill Run not found' })\n }\n const failedOutcomes = await em.find(BillingRunOutcome, {\n billRunId: parentRun.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n status: 'failed',\n })\n if (failedOutcomes.length === 0) {\n throw new CrudHttpError(409, {\n error: 'Parent run has no failed outcomes to retry',\n code: 'billing.run.nothing_to_retry',\n })\n }\n const scopedAccountIds = Array.from(new Set(failedOutcomes.map((o) => o.billAccountId)))\n\n try {\n const { billRun, outcomes } = await runBillRun(em, getContainer(ctx), {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n asOfDate: parentRun.asOfDate,\n triggeredBy: 'manual',\n mode: parsed.mode,\n scopedAccountIds,\n parentRunId: parentRun.id,\n catchUp: parentRun.catchUp,\n })\n return {\n billRunId: billRun.id,\n status: billRun.status,\n outcomesCount: outcomes.length,\n }\n } catch (err) {\n if (asLockBusyError(err)) {\n throw new CrudHttpError(409, {\n error: 'Another Bill Run is already in progress for this tenant',\n code: 'billing.run.lock_busy',\n tenantId: err.tenantId,\n lockName: err.lockName,\n })\n }\n throw err\n }\n },\n}\n\nregisterCommand(triggerRunCommand)\nregisterCommand(retryFailedCommand)\n\nexport { triggerRunCommand, retryFailedCommand }\n"],
|
|
5
|
+
"mappings": "AAGA,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAC9B,SAAS,YAAY,yBAAyB;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AAiBpC,SAAS,MAAM,KAA2C;AACxD,SAAQ,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC7D;AAEA,SAAS,aAAa,KAA6C;AACjE,SAAO,IAAI;AACb;AAEA,SAAS,gBAAgB,KAA0C;AACjE,SAAO,eAAe;AACxB;AAUA,MAAM,oBAA2E;AAAA,EAC/E,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,WAAW,OAAO,YAAY,oBAAI,KAAK;AAE7C,QAAI;AACF,YAAM,EAAE,SAAS,SAAS,IAAI,MAAM,WAAW,IAAI,aAAa,GAAG,GAAG;AAAA,QACpE,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB;AAAA,QACA,aAAa;AAAA,QACb,MAAM,OAAO;AAAA,QACb,kBAAkB,OAAO;AAAA,QACzB,SAAS,OAAO;AAAA,MAClB,CAAC;AACD,aAAO;AAAA,QACL,WAAW,QAAQ;AAAA,QACnB,QAAQ,QAAQ;AAAA,QAChB,eAAe,SAAS;AAAA,MAC1B;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,gBAAgB,GAAG,GAAG;AAKxB,cAAM,IAAI,cAAc,KAAK;AAAA,UAC3B,OAAO;AAAA,UACP,MAAM;AAAA,UACN,UAAU,IAAI;AAAA,UACd,UAAU,IAAI;AAAA,QAChB,CAAC;AAAA,MACH;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAIA,MAAM,qBAAgF;AAAA,EACpF,IAAI;AAAA,EAEJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,4BAA4B,MAAM,QAAQ;AACzD,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAElD,UAAM,KAAK,MAAM,GAAG;AACpB,UAAM,YAAY,MAAM,GAAG,QAAQ,YAAY;AAAA,MAC7C,IAAI,OAAO;AAAA,MACX,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,4BAA4B,CAAC;AAAA,IACrE;AACA,UAAM,iBAAiB,MAAM,GAAG,KAAK,mBAAmB;AAAA,MACtD,WAAW,UAAU;AAAA,MACrB,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,QAAQ;AAAA,IACV,CAAC;AACD,QAAI,eAAe,WAAW,GAAG;AAC/B,YAAM,IAAI,cAAc,KAAK;AAAA,QAC3B,OAAO;AAAA,QACP,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AACA,UAAM,mBAAmB,MAAM,KAAK,IAAI,IAAI,eAAe,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAEvF,QAAI;AACF,YAAM,EAAE,SAAS,SAAS,IAAI,MAAM,WAAW,IAAI,aAAa,GAAG,GAAG;AAAA,QACpE,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB,UAAU,UAAU;AAAA,QACpB,aAAa;AAAA,QACb,MAAM,OAAO;AAAA,QACb;AAAA,QACA,aAAa,UAAU;AAAA,QACvB,SAAS,UAAU;AAAA,MACrB,CAAC;AACD,aAAO;AAAA,QACL,WAAW,QAAQ;AAAA,QACnB,QAAQ,QAAQ;AAAA,QAChB,eAAe,SAAS;AAAA,MAC1B;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,gBAAgB,GAAG,GAAG;AACxB,cAAM,IAAI,cAAc,KAAK;AAAA,UAC3B,OAAO;AAAA,UACP,MAAM;AAAA,UACN,UAAU,IAAI;AAAA,UACd,UAAU,IAAI;AAAA,QAChB,CAAC;AAAA,MACH;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAEA,gBAAgB,iBAAiB;AACjC,gBAAgB,kBAAkB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/components/PrepaidPanel.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Plus, SlidersHorizontal } from 'lucide-react'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { StatCard, Stat } from './DetailFields'\n\n/**\n * Prepaid panel (SPEC-002 P4) injected into the Billing Account detail page\n * for `billing_mode='prepaid'` accounts: balance + status, credit status,\n * a top-up launcher, and the recent balance transactions.\n */\n\ntype BalanceSnapshot = {\n balance: string\n balanceStatus: 'ok' | 'low' | 'exhausted'\n lowBalanceThreshold: string\n lastMovementAt: string | null\n}\n\ntype CreditSnapshot = {\n mode: string\n creditLimit: string\n creditUsed: string\n availableCredit: string\n creditStatus: 'within_limit' | 'near_limit' | 'over_limit'\n}\n\ntype TransactionRow = {\n id: string\n type: string\n amount: string\n balance_after: string\n description: string\n created_at: string\n}\n\nfunction balanceVariant(status: string): 'success' | 'warning' | 'error' {\n if (status === 'low') return 'warning'\n if (status === 'exhausted') return 'error'\n return 'success'\n}\n\nfunction creditVariant(status: string): 'success' | 'warning' | 'error' {\n if (status === 'near_limit') return 'warning'\n if (status === 'over_limit') return 'error'\n return 'success'\n}\n\nfunction txnVariant(type: string): 'success' | 'info' | 'neutral' {\n if (type === 'topup' || type === 'adjustment') return 'success'\n if (type === 'usage') return 'info'\n return 'neutral'\n}\n\nfunction formatDateTime(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const d = new Date(value)\n return Number.isNaN(d.getTime()) ? value : d.toLocaleString()\n}\n\nexport function PrepaidPanel({\n accountId,\n currencyCode,\n}: {\n accountId: string\n currencyCode: string\n}) {\n const t = useT()\n const router = useRouter()\n const [balance, setBalance] = React.useState<BalanceSnapshot | null>(null)\n const [credit, setCredit] = React.useState<CreditSnapshot | null>(null)\n const [rows, setRows] = React.useState<TransactionRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const [topupOpen, setTopupOpen] = React.useState(false)\n const [amount, setAmount] = React.useState('')\n const [submitting, setSubmitting] = React.useState(false)\n const [adjustOpen, setAdjustOpen] = React.useState(false)\n const [adjAmount, setAdjAmount] = React.useState('')\n const [adjReason, setAdjReason] = React.useState('')\n const [adjSubmitting, setAdjSubmitting] = React.useState(false)\n const pageSize = 10\n\n const loadStatus = React.useCallback(async () => {\n const [bal, cred] = await Promise.all([\n readApiResultOrThrow<BalanceSnapshot>(`/api/billing/accounts/${accountId}/balance`),\n readApiResultOrThrow<CreditSnapshot>(`/api/billing/accounts/${accountId}/credit-status`),\n ])\n setBalance(bal)\n setCredit(cred)\n }, [accountId])\n\n const loadTransactions = React.useCallback(async () => {\n setLoading(true)\n try {\n const result = await readApiResultOrThrow<{\n items: TransactionRow[]\n total: number\n totalPages: number\n }>(\n `/api/billing/transactions?billAccountId=${accountId}&page=${page}` +\n `&pageSize=${pageSize}&sortField=createdAt&sortDir=desc`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [accountId, page])\n\n React.useEffect(() => {\n void loadStatus().catch(() => undefined)\n }, [loadStatus])\n React.useEffect(() => {\n void loadTransactions()\n }, [loadTransactions])\n\n const handleTopup = React.useCallback(async () => {\n const value = Number.parseFloat(amount)\n if (!Number.isFinite(value) || value <= 0) {\n flash(t('billing.prepaid.topup.invalid', 'Enter a positive amount'), 'error')\n return\n }\n setSubmitting(true)\n try {\n const res = await apiCallOrThrow<{ redirectUrl?: string | null }>(\n `/api/billing/accounts/${accountId}/topups`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n amount: value,\n successUrl: typeof window !== 'undefined' ? window.location.href : undefined,\n cancelUrl: typeof window !== 'undefined' ? window.location.href : undefined,\n }),\n },\n )\n const redirectUrl = res.result?.redirectUrl\n if (redirectUrl && typeof window !== 'undefined') {\n window.location.href = redirectUrl\n return\n }\n flash(t('billing.prepaid.topup.created', 'Top-up session created'), 'success')\n setTopupOpen(false)\n setAmount('')\n void loadStatus().catch(() => undefined)\n void loadTransactions()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.prepaid.topup.error', 'Top-up failed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [accountId, amount, loadStatus, loadTransactions, t])\n\n const handleAdjust = React.useCallback(async () => {\n const value = Number.parseFloat(adjAmount)\n if (!Number.isFinite(value) || value === 0) {\n flash(t('billing.prepaid.adjust.invalid_amount', 'Enter a non-zero amount'), 'error')\n return\n }\n if (!adjReason.trim()) {\n flash(t('billing.prepaid.adjust.invalid_description', 'Enter a reason'), 'error')\n return\n }\n setAdjSubmitting(true)\n try {\n await apiCallOrThrow('/api/billing/transactions/adjust', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n billAccountId: accountId,\n amount: value,\n description: adjReason.trim(),\n }),\n })\n flash(t('billing.prepaid.adjust.success', 'Balance adjusted'), 'success')\n setAdjustOpen(false)\n setAdjAmount('')\n setAdjReason('')\n void loadStatus().catch(() => undefined)\n void loadTransactions()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.prepaid.adjust.error', 'Adjustment failed'), 'error')\n } finally {\n setAdjSubmitting(false)\n }\n }, [accountId, adjAmount, adjReason, loadStatus, loadTransactions, t])\n\n const columns = React.useMemo<ColumnDef<TransactionRow>[]>(\n () => [\n {\n accessorKey: 'created_at',\n header: t('billing.transactions.columns.when', 'When'),\n cell: ({ row }) => formatDateTime(row.original.created_at),\n },\n {\n accessorKey: 'type',\n header: t('billing.transactions.columns.type', 'Type'),\n cell: ({ row }) => (\n <Tag variant={txnVariant(row.original.type)}>\n {t(`billing.transactions.type.${row.original.type}`, row.original.type)}\n </Tag>\n ),\n },\n {\n accessorKey: 'description',\n header: t('billing.transactions.columns.description', 'Description'),\n cell: ({ row }) => <span className=\"text-sm\">{row.original.description}</span>,\n },\n {\n accessorKey: 'amount',\n header: t('billing.transactions.columns.amount', 'Amount'),\n cell: ({ row }) => (\n <span className=\"font-mono tabular-nums\">{row.original.amount}</span>\n ),\n },\n {\n accessorKey: 'balance_after',\n header: t('billing.transactions.columns.balance_after', 'Balance'),\n cell: ({ row }) => (\n <span className=\"font-mono tabular-nums\">{row.original.balance_after}</span>\n ),\n },\n ],\n [t],\n )\n\n return (\n <div className=\"mt-6 space-y-4\">\n <StatCard title={t('billing.prepaid.panel.title', 'Prepaid balance')}>\n <Stat\n label={t('billing.prepaid.panel.balance', 'Balance')}\n value={\n <span className=\"flex items-center gap-2\">\n <span className=\"font-mono tabular-nums\">\n {balance?.balance ?? '\u2014'} {currencyCode}\n </span>\n {balance ? (\n <Tag variant={balanceVariant(balance.balanceStatus)}>\n {t(`billing.prepaid.status.${balance.balanceStatus}`, balance.balanceStatus)}\n </Tag>\n ) : null}\n </span>\n }\n />\n <Stat\n label={t('billing.prepaid.panel.threshold', 'Low threshold')}\n value={\n <span className=\"font-mono tabular-nums\">{balance?.lowBalanceThreshold ?? '\u2014'}</span>\n }\n />\n {credit ? (\n <Stat\n label={t('billing.prepaid.panel.credit', 'Credit')}\n value={\n <span className=\"flex items-center gap-2\">\n <span className=\"font-mono tabular-nums\">\n {credit.creditUsed} / {credit.creditLimit}\n </span>\n <Tag variant={creditVariant(credit.creditStatus)}>\n {t(`billing.prepaid.credit.${credit.creditStatus}`, credit.creditStatus)}\n </Tag>\n </span>\n }\n />\n ) : null}\n <Stat\n label={t('billing.prepaid.panel.last_movement', 'Last movement')}\n value={formatDateTime(balance?.lastMovementAt)}\n />\n </StatCard>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n {!topupOpen && !adjustOpen ? (\n <>\n <Button type=\"button\" onClick={() => setTopupOpen(true)}>\n <Plus size={16} />\n {t('billing.prepaid.topup.action', 'Top up')}\n </Button>\n <Button type=\"button\" variant=\"outline\" onClick={() => setAdjustOpen(true)}>\n <SlidersHorizontal size={16} />\n {t('billing.prepaid.adjust.action', 'Adjust')}\n </Button>\n </>\n ) : null}\n\n {topupOpen ? (\n <div className=\"flex items-center gap-2\">\n <input\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n autoFocus\n value={amount}\n onChange={(e) => setAmount(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') void handleTopup()\n if (e.key === 'Escape') setTopupOpen(false)\n }}\n placeholder={t('billing.prepaid.topup.amount', 'Amount')}\n className=\"h-9 w-32 rounded-md border border-border bg-background px-3 text-sm\"\n />\n <Button type=\"button\" onClick={handleTopup} disabled={submitting}>\n {submitting\n ? t('billing.prepaid.topup.submitting', 'Starting\u2026')\n : t('billing.prepaid.topup.confirm', 'Start checkout')}\n </Button>\n <Button type=\"button\" variant=\"outline\" onClick={() => setTopupOpen(false)}>\n {t('billing.common.cancel', 'Cancel')}\n </Button>\n </div>\n ) : null}\n\n {adjustOpen ? (\n <div className=\"flex flex-wrap items-center gap-2\">\n <input\n type=\"number\"\n step=\"0.01\"\n autoFocus\n value={adjAmount}\n onChange={(e) => setAdjAmount(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') void handleAdjust()\n if (e.key === 'Escape') setAdjustOpen(false)\n }}\n placeholder={t('billing.prepaid.adjust.amount', 'Amount (+credit / \u2212debit)')}\n className=\"h-9 w-48 rounded-md border border-border bg-background px-3 text-sm\"\n />\n <input\n type=\"text\"\n value={adjReason}\n onChange={(e) => setAdjReason(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') void handleAdjust()\n if (e.key === 'Escape') setAdjustOpen(false)\n }}\n placeholder={t('billing.prepaid.adjust.description', 'Reason')}\n className=\"h-9 w-64 rounded-md border border-border bg-background px-3 text-sm\"\n />\n <Button type=\"button\" onClick={handleAdjust} disabled={adjSubmitting}>\n {adjSubmitting\n ? t('billing.prepaid.adjust.submitting', 'Applying\u2026')\n : t('billing.prepaid.adjust.confirm', 'Apply adjustment')}\n </Button>\n <Button type=\"button\" variant=\"outline\" onClick={() => setAdjustOpen(false)}>\n {t('billing.common.cancel', 'Cancel')}\n </Button>\n </div>\n ) : null}\n </div>\n\n <DataTable\n title={t('billing.prepaid.panel.transactions', 'Balance transactions')}\n columns={columns}\n data={rows}\n isLoading={loading}\n onRowClick={(row) => router.push(`/backend/billing/transactions/${row.id}`)}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('billing.prepaid.panel.no_transactions', 'No balance movements yet.')}\n </div>\n }\n />\n </div>\n )\n}\n\nexport default PrepaidPanel\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Plus, SlidersHorizontal } from 'lucide-react'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { StatCard, Stat } from './DetailFields'\n\n/**\n * Prepaid panel (SPEC-002 P4) injected into the Billing Account detail page\n * for `billing_mode='prepaid'` accounts: balance + status, credit status,\n * a top-up launcher, and the recent balance transactions.\n */\n\ntype BalanceSnapshot = {\n balance: string\n balanceStatus: 'ok' | 'low' | 'exhausted'\n lowBalanceThreshold: string\n lastMovementAt: string | null\n}\n\ntype CreditSnapshot = {\n mode: string\n creditLimit: string\n creditUsed: string\n availableCredit: string\n creditStatus: 'within_limit' | 'near_limit' | 'over_limit'\n}\n\ntype TransactionRow = {\n id: string\n type: string\n amount: string\n balance_after: string\n description: string\n created_at: string\n}\n\nfunction balanceVariant(status: string): 'success' | 'warning' | 'error' {\n if (status === 'low') return 'warning'\n if (status === 'exhausted') return 'error'\n return 'success'\n}\n\nfunction creditVariant(status: string): 'success' | 'warning' | 'error' {\n if (status === 'near_limit') return 'warning'\n if (status === 'over_limit') return 'error'\n return 'success'\n}\n\nfunction txnVariant(type: string): 'success' | 'info' | 'neutral' {\n if (type === 'topup' || type === 'adjustment') return 'success'\n if (type === 'usage') return 'info'\n return 'neutral'\n}\n\nfunction formatDateTime(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const d = new Date(value)\n return Number.isNaN(d.getTime()) ? value : d.toLocaleString()\n}\n\nexport function PrepaidPanel({\n accountId,\n currencyCode,\n}: {\n accountId: string\n currencyCode: string\n}) {\n const t = useT()\n const router = useRouter()\n const [balance, setBalance] = React.useState<BalanceSnapshot | null>(null)\n const [credit, setCredit] = React.useState<CreditSnapshot | null>(null)\n const [rows, setRows] = React.useState<TransactionRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const [topupOpen, setTopupOpen] = React.useState(false)\n const [amount, setAmount] = React.useState('')\n const [submitting, setSubmitting] = React.useState(false)\n const [adjustOpen, setAdjustOpen] = React.useState(false)\n const [adjAmount, setAdjAmount] = React.useState('')\n const [adjReason, setAdjReason] = React.useState('')\n const [adjSubmitting, setAdjSubmitting] = React.useState(false)\n const pageSize = 10\n\n const loadStatus = React.useCallback(async () => {\n const [bal, cred] = await Promise.all([\n readApiResultOrThrow<BalanceSnapshot>(`/api/billing/accounts/${accountId}/balance`),\n readApiResultOrThrow<CreditSnapshot>(`/api/billing/accounts/${accountId}/credit-status`),\n ])\n setBalance(bal)\n setCredit(cred)\n }, [accountId])\n\n const loadTransactions = React.useCallback(async () => {\n setLoading(true)\n try {\n const result = await readApiResultOrThrow<{\n items: TransactionRow[]\n total: number\n totalPages: number\n }>(\n `/api/billing/transactions?billAccountId=${accountId}&page=${page}` +\n `&pageSize=${pageSize}&sortField=createdAt&sortDir=desc`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [accountId, page])\n\n React.useEffect(() => {\n void loadStatus().catch(() => undefined)\n }, [loadStatus])\n React.useEffect(() => {\n void loadTransactions()\n }, [loadTransactions])\n\n const handleTopup = React.useCallback(async () => {\n const value = Number.parseFloat(amount)\n if (!Number.isFinite(value) || value <= 0) {\n flash(t('billing.prepaid.topup.invalid', 'Enter a positive amount'), 'error')\n return\n }\n setSubmitting(true)\n try {\n const res = await apiCallOrThrow<{ redirectUrl?: string | null }>(\n `/api/billing/accounts/${accountId}/topups`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n amount: value,\n successUrl: typeof window !== 'undefined' ? window.location.href : undefined,\n cancelUrl: typeof window !== 'undefined' ? window.location.href : undefined,\n }),\n },\n )\n const redirectUrl = res.result?.redirectUrl\n if (redirectUrl && typeof window !== 'undefined') {\n window.location.href = redirectUrl\n return\n }\n flash(t('billing.prepaid.topup.created', 'Top-up session created'), 'success')\n setTopupOpen(false)\n setAmount('')\n void loadStatus().catch(() => undefined)\n void loadTransactions()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.prepaid.topup.error', 'Top-up failed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [accountId, amount, loadStatus, loadTransactions, t])\n\n const handleAdjust = React.useCallback(async () => {\n const value = Number.parseFloat(adjAmount)\n if (!Number.isFinite(value) || value === 0) {\n flash(t('billing.prepaid.adjust.invalid_amount', 'Enter a non-zero amount'), 'error')\n return\n }\n if (!adjReason.trim()) {\n flash(t('billing.prepaid.adjust.invalid_description', 'Enter a reason'), 'error')\n return\n }\n setAdjSubmitting(true)\n try {\n await apiCallOrThrow('/api/billing/transactions/adjust', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n billAccountId: accountId,\n amount: value,\n description: adjReason.trim(),\n }),\n })\n flash(t('billing.prepaid.adjust.success', 'Balance adjusted'), 'success')\n setAdjustOpen(false)\n setAdjAmount('')\n setAdjReason('')\n void loadStatus().catch(() => undefined)\n void loadTransactions()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.prepaid.adjust.error', 'Adjustment failed'), 'error')\n } finally {\n setAdjSubmitting(false)\n }\n }, [accountId, adjAmount, adjReason, loadStatus, loadTransactions, t])\n\n const columns = React.useMemo<ColumnDef<TransactionRow>[]>(\n () => [\n {\n accessorKey: 'created_at',\n header: t('billing.transactions.columns.when', 'When'),\n cell: ({ row }) => formatDateTime(row.original.created_at),\n },\n {\n accessorKey: 'type',\n header: t('billing.transactions.columns.type', 'Type'),\n cell: ({ row }) => (\n <Tag variant={txnVariant(row.original.type)}>\n {t(`billing.transactions.type.${row.original.type}`, row.original.type)}\n </Tag>\n ),\n },\n {\n accessorKey: 'description',\n header: t('billing.transactions.columns.description', 'Description'),\n cell: ({ row }) => <span className=\"text-sm\">{row.original.description}</span>,\n },\n {\n accessorKey: 'amount',\n header: t('billing.transactions.columns.amount', 'Amount'),\n cell: ({ row }) => (\n <span className=\"font-mono tabular-nums\">{row.original.amount}</span>\n ),\n },\n {\n accessorKey: 'balance_after',\n header: t('billing.transactions.columns.balance_after', 'Balance'),\n cell: ({ row }) => (\n <span className=\"font-mono tabular-nums\">{row.original.balance_after}</span>\n ),\n },\n ],\n [t],\n )\n\n return (\n <div className=\"mt-6 space-y-4\">\n <StatCard title={t('billing.prepaid.panel.title', 'Prepaid balance')}>\n <Stat\n label={t('billing.prepaid.panel.balance', 'Balance')}\n value={\n <span className=\"flex items-center gap-2\">\n <span className=\"font-mono tabular-nums\">\n {balance?.balance ?? '\u2014'} {currencyCode}\n </span>\n {balance ? (\n <Tag variant={balanceVariant(balance.balanceStatus)}>\n {t(`billing.prepaid.status.${balance.balanceStatus}`, balance.balanceStatus)}\n </Tag>\n ) : null}\n </span>\n }\n />\n <Stat\n label={t('billing.prepaid.panel.threshold', 'Low threshold')}\n value={\n <span className=\"font-mono tabular-nums\">{balance?.lowBalanceThreshold ?? '\u2014'}</span>\n }\n />\n {credit ? (\n <Stat\n label={t('billing.prepaid.panel.credit', 'Credit')}\n value={\n <span className=\"flex items-center gap-2\">\n <span className=\"font-mono tabular-nums\">\n {credit.creditUsed} / {credit.creditLimit}\n </span>\n <Tag variant={creditVariant(credit.creditStatus)}>\n {t(`billing.prepaid.credit.${credit.creditStatus}`, credit.creditStatus)}\n </Tag>\n </span>\n }\n />\n ) : null}\n <Stat\n label={t('billing.prepaid.panel.last_movement', 'Last movement')}\n value={formatDateTime(balance?.lastMovementAt)}\n />\n </StatCard>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n {!topupOpen && !adjustOpen ? (\n <>\n <Button type=\"button\" onClick={() => setTopupOpen(true)}>\n <Plus size={16} />\n {t('billing.prepaid.topup.action', 'Top up')}\n </Button>\n <Button type=\"button\" variant=\"outline\" onClick={() => setAdjustOpen(true)}>\n <SlidersHorizontal size={16} />\n {t('billing.prepaid.adjust.action', 'Adjust')}\n </Button>\n </>\n ) : null}\n\n {topupOpen ? (\n <div className=\"flex items-center gap-2\">\n <input\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n autoFocus\n value={amount}\n onChange={(e) => setAmount(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') void handleTopup()\n if (e.key === 'Escape') setTopupOpen(false)\n }}\n placeholder={t('billing.prepaid.topup.amount', 'Amount')}\n className=\"h-9 w-32 rounded-md border border-border bg-background px-3 text-sm\"\n />\n <Button type=\"button\" onClick={handleTopup} disabled={submitting}>\n {submitting\n ? t('billing.prepaid.topup.submitting', 'Starting\u2026')\n : t('billing.prepaid.topup.confirm', 'Start checkout')}\n </Button>\n <Button type=\"button\" variant=\"outline\" onClick={() => setTopupOpen(false)}>\n {t('billing.common.cancel', 'Cancel')}\n </Button>\n </div>\n ) : null}\n\n {adjustOpen ? (\n <div className=\"flex flex-wrap items-center gap-2\">\n <input\n type=\"number\"\n step=\"0.01\"\n autoFocus\n value={adjAmount}\n onChange={(e) => setAdjAmount(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') void handleAdjust()\n if (e.key === 'Escape') setAdjustOpen(false)\n }}\n placeholder={t('billing.prepaid.adjust.amount', 'Amount (+credit / \u2212debit)')}\n className=\"h-9 w-48 rounded-md border border-border bg-background px-3 text-sm\"\n />\n <input\n type=\"text\"\n value={adjReason}\n onChange={(e) => setAdjReason(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') void handleAdjust()\n if (e.key === 'Escape') setAdjustOpen(false)\n }}\n placeholder={t('billing.prepaid.adjust.description', 'Reason')}\n className=\"h-9 w-64 rounded-md border border-border bg-background px-3 text-sm\"\n />\n <Button type=\"button\" onClick={handleAdjust} disabled={adjSubmitting}>\n {adjSubmitting\n ? t('billing.prepaid.adjust.submitting', 'Applying\u2026')\n : t('billing.prepaid.adjust.confirm', 'Apply adjustment')}\n </Button>\n <Button type=\"button\" variant=\"outline\" onClick={() => setAdjustOpen(false)}>\n {t('billing.common.cancel', 'Cancel')}\n </Button>\n </div>\n ) : null}\n </div>\n\n <DataTable\n title={t('billing.prepaid.panel.transactions', 'Balance transactions')}\n columns={columns}\n data={rows}\n isLoading={loading}\n onRowClick={(row) => router.push(`/backend/billing/transactions/${row.id}`)}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('billing.prepaid.panel.no_transactions', 'No balance movements yet.')}\n </div>\n }\n />\n </div>\n )\n}\n\nexport default PrepaidPanel\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -247,7 +247,7 @@ BillingAccountUsage = __decorateClass([
|
|
|
247
247
|
})
|
|
248
248
|
], BillingAccountUsage);
|
|
249
249
|
OptionalProps;
|
|
250
|
-
let
|
|
250
|
+
let BillingRun = class {
|
|
251
251
|
constructor() {
|
|
252
252
|
this.dryRun = false;
|
|
253
253
|
this.testMode = false;
|
|
@@ -259,59 +259,59 @@ let BillRun = class {
|
|
|
259
259
|
};
|
|
260
260
|
__decorateClass([
|
|
261
261
|
PrimaryKey({ type: "uuid", defaultRaw: "gen_random_uuid()" })
|
|
262
|
-
],
|
|
262
|
+
], BillingRun.prototype, "id", 2);
|
|
263
263
|
__decorateClass([
|
|
264
264
|
Property({ name: "organization_id", type: "uuid" })
|
|
265
|
-
],
|
|
265
|
+
], BillingRun.prototype, "organizationId", 2);
|
|
266
266
|
__decorateClass([
|
|
267
267
|
Property({ name: "tenant_id", type: "uuid" })
|
|
268
|
-
],
|
|
268
|
+
], BillingRun.prototype, "tenantId", 2);
|
|
269
269
|
__decorateClass([
|
|
270
270
|
Property({ name: "triggered_by", type: "text" })
|
|
271
|
-
],
|
|
271
|
+
], BillingRun.prototype, "triggeredBy", 2);
|
|
272
272
|
__decorateClass([
|
|
273
273
|
Property({ name: "parent_run_id", type: "uuid", nullable: true })
|
|
274
|
-
],
|
|
274
|
+
], BillingRun.prototype, "parentRunId", 2);
|
|
275
275
|
__decorateClass([
|
|
276
276
|
Property({ name: "dry_run", type: "boolean", default: false })
|
|
277
|
-
],
|
|
277
|
+
], BillingRun.prototype, "dryRun", 2);
|
|
278
278
|
__decorateClass([
|
|
279
279
|
Property({ name: "test_mode", type: "boolean", default: false })
|
|
280
|
-
],
|
|
280
|
+
], BillingRun.prototype, "testMode", 2);
|
|
281
281
|
__decorateClass([
|
|
282
282
|
Property({ name: "catch_up", type: "boolean", default: false })
|
|
283
|
-
],
|
|
283
|
+
], BillingRun.prototype, "catchUp", 2);
|
|
284
284
|
__decorateClass([
|
|
285
285
|
Property({ name: "scoped_account_ids", type: "jsonb", nullable: true })
|
|
286
|
-
],
|
|
286
|
+
], BillingRun.prototype, "scopedAccountIds", 2);
|
|
287
287
|
__decorateClass([
|
|
288
288
|
Property({ name: "as_of_date", type: "date", columnType: "date" })
|
|
289
|
-
],
|
|
289
|
+
], BillingRun.prototype, "asOfDate", 2);
|
|
290
290
|
__decorateClass([
|
|
291
291
|
Property({ name: "started_at", type: Date, nullable: true })
|
|
292
|
-
],
|
|
292
|
+
], BillingRun.prototype, "startedAt", 2);
|
|
293
293
|
__decorateClass([
|
|
294
294
|
Property({ name: "finished_at", type: Date, nullable: true })
|
|
295
|
-
],
|
|
295
|
+
], BillingRun.prototype, "finishedAt", 2);
|
|
296
296
|
__decorateClass([
|
|
297
297
|
Property({ type: "text" })
|
|
298
|
-
],
|
|
298
|
+
], BillingRun.prototype, "status", 2);
|
|
299
299
|
__decorateClass([
|
|
300
300
|
Property({ type: "jsonb", nullable: true })
|
|
301
|
-
],
|
|
301
|
+
], BillingRun.prototype, "summary", 2);
|
|
302
302
|
__decorateClass([
|
|
303
303
|
Property({ name: "is_active", type: "boolean", default: true })
|
|
304
|
-
],
|
|
304
|
+
], BillingRun.prototype, "isActive", 2);
|
|
305
305
|
__decorateClass([
|
|
306
306
|
Property({ name: "created_at", type: Date, onCreate: () => /* @__PURE__ */ new Date() })
|
|
307
|
-
],
|
|
307
|
+
], BillingRun.prototype, "createdAt", 2);
|
|
308
308
|
__decorateClass([
|
|
309
309
|
Property({ name: "updated_at", type: Date, onUpdate: () => /* @__PURE__ */ new Date() })
|
|
310
|
-
],
|
|
310
|
+
], BillingRun.prototype, "updatedAt", 2);
|
|
311
311
|
__decorateClass([
|
|
312
312
|
Property({ name: "deleted_at", type: Date, nullable: true })
|
|
313
|
-
],
|
|
314
|
-
|
|
313
|
+
], BillingRun.prototype, "deletedAt", 2);
|
|
314
|
+
BillingRun = __decorateClass([
|
|
315
315
|
Entity({ tableName: "billing_runs" }),
|
|
316
316
|
Index({
|
|
317
317
|
name: "billing_runs_tenant_status_idx",
|
|
@@ -321,9 +321,9 @@ BillRun = __decorateClass([
|
|
|
321
321
|
name: "billing_runs_parent_idx",
|
|
322
322
|
properties: ["parentRunId"]
|
|
323
323
|
})
|
|
324
|
-
],
|
|
324
|
+
], BillingRun);
|
|
325
325
|
OptionalProps;
|
|
326
|
-
let
|
|
326
|
+
let BillingRunOutcome = class {
|
|
327
327
|
constructor() {
|
|
328
328
|
this.isActive = true;
|
|
329
329
|
this.createdAt = /* @__PURE__ */ new Date();
|
|
@@ -332,47 +332,47 @@ let BillRunOutcome = class {
|
|
|
332
332
|
};
|
|
333
333
|
__decorateClass([
|
|
334
334
|
PrimaryKey({ type: "uuid", defaultRaw: "gen_random_uuid()" })
|
|
335
|
-
],
|
|
335
|
+
], BillingRunOutcome.prototype, "id", 2);
|
|
336
336
|
__decorateClass([
|
|
337
337
|
Property({ name: "organization_id", type: "uuid" })
|
|
338
|
-
],
|
|
338
|
+
], BillingRunOutcome.prototype, "organizationId", 2);
|
|
339
339
|
__decorateClass([
|
|
340
340
|
Property({ name: "tenant_id", type: "uuid" })
|
|
341
|
-
],
|
|
341
|
+
], BillingRunOutcome.prototype, "tenantId", 2);
|
|
342
342
|
__decorateClass([
|
|
343
343
|
Property({ name: "bill_run_id", type: "uuid" })
|
|
344
|
-
],
|
|
344
|
+
], BillingRunOutcome.prototype, "billRunId", 2);
|
|
345
345
|
__decorateClass([
|
|
346
346
|
Property({ name: "bill_account_id", type: "uuid" })
|
|
347
|
-
],
|
|
347
|
+
], BillingRunOutcome.prototype, "billAccountId", 2);
|
|
348
348
|
__decorateClass([
|
|
349
349
|
Property({ type: "text" })
|
|
350
|
-
],
|
|
350
|
+
], BillingRunOutcome.prototype, "status", 2);
|
|
351
351
|
__decorateClass([
|
|
352
352
|
Property({ name: "error_message", type: "text", nullable: true })
|
|
353
|
-
],
|
|
353
|
+
], BillingRunOutcome.prototype, "errorMessage", 2);
|
|
354
354
|
__decorateClass([
|
|
355
355
|
Property({ type: "jsonb", nullable: true })
|
|
356
|
-
],
|
|
356
|
+
], BillingRunOutcome.prototype, "warnings", 2);
|
|
357
357
|
__decorateClass([
|
|
358
358
|
Property({ name: "draft_invoice_id", type: "uuid", nullable: true })
|
|
359
|
-
],
|
|
359
|
+
], BillingRunOutcome.prototype, "draftInvoiceId", 2);
|
|
360
360
|
__decorateClass([
|
|
361
361
|
Property({ name: "statement_id", type: "uuid", nullable: true })
|
|
362
|
-
],
|
|
362
|
+
], BillingRunOutcome.prototype, "statementId", 2);
|
|
363
363
|
__decorateClass([
|
|
364
364
|
Property({ name: "is_active", type: "boolean", default: true })
|
|
365
|
-
],
|
|
365
|
+
], BillingRunOutcome.prototype, "isActive", 2);
|
|
366
366
|
__decorateClass([
|
|
367
367
|
Property({ name: "created_at", type: Date, onCreate: () => /* @__PURE__ */ new Date() })
|
|
368
|
-
],
|
|
368
|
+
], BillingRunOutcome.prototype, "createdAt", 2);
|
|
369
369
|
__decorateClass([
|
|
370
370
|
Property({ name: "updated_at", type: Date, onUpdate: () => /* @__PURE__ */ new Date() })
|
|
371
|
-
],
|
|
371
|
+
], BillingRunOutcome.prototype, "updatedAt", 2);
|
|
372
372
|
__decorateClass([
|
|
373
373
|
Property({ name: "deleted_at", type: Date, nullable: true })
|
|
374
|
-
],
|
|
375
|
-
|
|
374
|
+
], BillingRunOutcome.prototype, "deletedAt", 2);
|
|
375
|
+
BillingRunOutcome = __decorateClass([
|
|
376
376
|
Entity({ tableName: "billing_run_outcomes" }),
|
|
377
377
|
Index({
|
|
378
378
|
name: "billing_run_outcomes_run_status_idx",
|
|
@@ -382,47 +382,47 @@ BillRunOutcome = __decorateClass([
|
|
|
382
382
|
name: "billing_run_outcomes_account_history_idx",
|
|
383
383
|
properties: ["organizationId", "tenantId", "billAccountId", "createdAt"]
|
|
384
384
|
})
|
|
385
|
-
],
|
|
385
|
+
], BillingRunOutcome);
|
|
386
386
|
OptionalProps;
|
|
387
|
-
let
|
|
387
|
+
let BillingDraftInvoiceEdit = class {
|
|
388
388
|
constructor() {
|
|
389
389
|
this.createdAt = /* @__PURE__ */ new Date();
|
|
390
390
|
}
|
|
391
391
|
};
|
|
392
392
|
__decorateClass([
|
|
393
393
|
PrimaryKey({ type: "uuid", defaultRaw: "gen_random_uuid()" })
|
|
394
|
-
],
|
|
394
|
+
], BillingDraftInvoiceEdit.prototype, "id", 2);
|
|
395
395
|
__decorateClass([
|
|
396
396
|
Property({ name: "organization_id", type: "uuid" })
|
|
397
|
-
],
|
|
397
|
+
], BillingDraftInvoiceEdit.prototype, "organizationId", 2);
|
|
398
398
|
__decorateClass([
|
|
399
399
|
Property({ name: "tenant_id", type: "uuid" })
|
|
400
|
-
],
|
|
400
|
+
], BillingDraftInvoiceEdit.prototype, "tenantId", 2);
|
|
401
401
|
__decorateClass([
|
|
402
402
|
Property({ name: "invoice_id", type: "uuid" })
|
|
403
|
-
],
|
|
403
|
+
], BillingDraftInvoiceEdit.prototype, "invoiceId", 2);
|
|
404
404
|
__decorateClass([
|
|
405
405
|
Property({ name: "invoice_line_id", type: "uuid", nullable: true })
|
|
406
|
-
],
|
|
406
|
+
], BillingDraftInvoiceEdit.prototype, "invoiceLineId", 2);
|
|
407
407
|
__decorateClass([
|
|
408
408
|
Property({ name: "user_id", type: "uuid" })
|
|
409
|
-
],
|
|
409
|
+
], BillingDraftInvoiceEdit.prototype, "userId", 2);
|
|
410
410
|
__decorateClass([
|
|
411
411
|
Property({ type: "text" })
|
|
412
|
-
],
|
|
412
|
+
], BillingDraftInvoiceEdit.prototype, "action", 2);
|
|
413
413
|
__decorateClass([
|
|
414
414
|
Property({ name: "before_json", type: "jsonb", nullable: true })
|
|
415
|
-
],
|
|
415
|
+
], BillingDraftInvoiceEdit.prototype, "beforeJson", 2);
|
|
416
416
|
__decorateClass([
|
|
417
417
|
Property({ name: "after_json", type: "jsonb", nullable: true })
|
|
418
|
-
],
|
|
418
|
+
], BillingDraftInvoiceEdit.prototype, "afterJson", 2);
|
|
419
419
|
__decorateClass([
|
|
420
420
|
Property({ name: "created_at", type: Date, onCreate: () => /* @__PURE__ */ new Date() })
|
|
421
|
-
],
|
|
421
|
+
], BillingDraftInvoiceEdit.prototype, "createdAt", 2);
|
|
422
422
|
__decorateClass([
|
|
423
423
|
Property({ name: "deleted_at", type: Date, nullable: true })
|
|
424
|
-
],
|
|
425
|
-
|
|
424
|
+
], BillingDraftInvoiceEdit.prototype, "deletedAt", 2);
|
|
425
|
+
BillingDraftInvoiceEdit = __decorateClass([
|
|
426
426
|
Entity({ tableName: "billing_draft_invoice_edits" }),
|
|
427
427
|
Index({
|
|
428
428
|
name: "billing_draft_invoice_edits_invoice_idx",
|
|
@@ -432,7 +432,7 @@ DraftInvoiceEdit = __decorateClass([
|
|
|
432
432
|
name: "billing_draft_invoice_edits_user_idx",
|
|
433
433
|
properties: ["organizationId", "tenantId", "userId", "createdAt"]
|
|
434
434
|
})
|
|
435
|
-
],
|
|
435
|
+
], BillingDraftInvoiceEdit);
|
|
436
436
|
OptionalProps;
|
|
437
437
|
let BillingAccountBalance = class {
|
|
438
438
|
constructor() {
|
|
@@ -709,15 +709,15 @@ BillingStatement = __decorateClass([
|
|
|
709
709
|
})
|
|
710
710
|
], BillingStatement);
|
|
711
711
|
export {
|
|
712
|
-
BillRun,
|
|
713
|
-
BillRunOutcome,
|
|
714
712
|
BillingAccount,
|
|
715
713
|
BillingAccountBalance,
|
|
716
714
|
BillingAccountTransaction,
|
|
717
715
|
BillingAccountUsage,
|
|
716
|
+
BillingDraftInvoiceEdit,
|
|
718
717
|
BillingItem,
|
|
718
|
+
BillingRun,
|
|
719
|
+
BillingRunOutcome,
|
|
719
720
|
BillingStatement,
|
|
720
|
-
BillingTopup
|
|
721
|
-
DraftInvoiceEdit
|
|
721
|
+
BillingTopup
|
|
722
722
|
};
|
|
723
723
|
//# sourceMappingURL=entities.js.map
|