@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/backend/billing/runs/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useParams } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\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 { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport {\n apiCallOrThrow,\n readApiResultOrThrow,\n} from '@open-mercato/ui/backend/utils/apiCall'\nimport { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { billRunName } from '../../../../lib/billRunLabel'\nimport {\n DetailCard,\n DetailField,\n Stat,\n StatCard,\n} from '../../../../components/DetailFields'\n\n/**\n * Bill Run detail \u2014 header card + outcomes list.\n *\n * The \"Retry failed accounts\" button is enabled only when the run\n * has at least one failed outcome. It POSTs the\n * `billing.runs.retry_failed` endpoint, which creates a new run\n * scoped to those accounts (the new run shows up in the list at\n * `/backend/billing/runs`).\n */\n\n// API list rows are snake_case (see `api/runs/route.ts` +\n// `api/run-outcomes/route.ts` `fields`).\ntype BillRun = {\n id: string\n triggered_by: 'schedule' | 'manual'\n parent_run_id: string | null\n dry_run: boolean\n test_mode: boolean\n catch_up: boolean\n as_of_date: string\n started_at: string | null\n finished_at: string | null\n status: 'running' | 'completed' | 'partial_failure' | 'failed'\n summary: {\n accounts_processed?: number\n drafts_created?: number\n drafts_skipped_existing?: number\n accounts_failed?: number\n accounts_with_warnings?: number\n usage_records_rated?: number\n } | null\n organization_id: string\n tenant_id: string\n created_at: string\n updated_at: string\n}\n\ntype BillRunOutcome = {\n id: string\n bill_run_id: string\n bill_account_id: string\n bill_account_name: string | null\n status: 'success' | 'success_with_warnings' | 'skipped_existing_draft' | 'failed'\n error_message: string | null\n warnings: Record<string, unknown> | null\n draft_invoice_id: string | null\n invoice_number: string | null\n created_at: string\n}\n\ntype ListResponse<T> = {\n items: T[]\n total: number\n totalPages: number\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString()\n}\n\nfunction runStatusVariant(\n status: BillRun['status'],\n): 'default' | 'success' | 'warning' | 'error' {\n switch (status) {\n case 'completed':\n return 'success'\n case 'partial_failure':\n return 'warning'\n case 'failed':\n return 'error'\n default:\n return 'default'\n }\n}\n\nfunction outcomeStatusVariant(\n status: BillRunOutcome['status'],\n): 'default' | 'success' | 'warning' | 'error' {\n switch (status) {\n case 'success':\n return 'success'\n case 'success_with_warnings':\n return 'warning'\n case 'failed':\n return 'error'\n case 'skipped_existing_draft':\n default:\n return 'default'\n }\n}\n\nexport default function BillRunDetailPage(props: { params?: { id?: string } }) {\n const t = useT()\n // OM serves backend pages through a catch-all route \u2014 the dynamic\n // `[id]` segment arrives as a page prop; `useParams()` is the fallback.\n const urlParams = useParams<{ id: string }>()\n const runId = (props.params?.id ?? urlParams?.id ?? '') as string\n\n const [run, setRun] = React.useState<BillRun | null>(null)\n const [runError, setRunError] = React.useState<string | null>(null)\n const [outcomes, setOutcomes] = React.useState<BillRunOutcome[]>([])\n const [outcomesPage, setOutcomesPage] = React.useState(1)\n const [outcomesTotal, setOutcomesTotal] = React.useState(0)\n const [outcomesTotalPages, setOutcomesTotalPages] = React.useState(1)\n const [outcomesLoading, setOutcomesLoading] = React.useState(true)\n const [retrying, setRetrying] = React.useState(false)\n\n const loadRun = React.useCallback(async () => {\n if (!runId) return\n setRunError(null)\n try {\n const result = await readApiResultOrThrow<ListResponse<BillRun>>(\n `/api/billing/runs?id=${runId}&pageSize=1`,\n )\n const found = result.items?.[0]\n if (!found) {\n setRunError(t('billing.runs.detail.error.not_found', 'Run not found'))\n setRun(null)\n return\n }\n setRun(found)\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n setRunError(message || t('billing.runs.detail.error.load', 'Failed to load run'))\n }\n }, [runId, t])\n\n const loadOutcomes = React.useCallback(async () => {\n if (!runId) return\n setOutcomesLoading(true)\n try {\n const params = new URLSearchParams({\n billRunId: runId,\n page: String(outcomesPage),\n pageSize: '50',\n })\n const result = await readApiResultOrThrow<ListResponse<BillRunOutcome>>(\n `/api/billing/run-outcomes?${params.toString()}`,\n )\n setOutcomes(result.items ?? [])\n setOutcomesTotal(result.total ?? 0)\n setOutcomesTotalPages(result.totalPages ?? 1)\n } finally {\n setOutcomesLoading(false)\n }\n }, [outcomesPage, runId])\n\n React.useEffect(() => {\n void loadRun()\n }, [loadRun])\n React.useEffect(() => {\n void loadOutcomes()\n }, [loadOutcomes])\n\n const failedCount = run?.summary?.accounts_failed ?? 0\n\n const handleRetryFailed = React.useCallback(async () => {\n if (!run || failedCount === 0) return\n setRetrying(true)\n try {\n const call = await apiCallOrThrow<{ billRunId: string; outcomesCount: number }>(\n '/api/billing/runs/retry-failed',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ billRunId: run.id }),\n },\n )\n const outcomesCount = call.result?.outcomesCount ?? 0\n flash(\n t('billing.runs.retry.success', 'Retry run created') +\n ` (${outcomesCount} accounts)`,\n 'success',\n )\n // Refresh \u2014 the new run shows up in the list view; the current\n // run's outcomes are immutable so no reload of `loadOutcomes`\n // needed, but loadRun is cheap.\n void loadRun()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.runs.retry.error', 'Retry failed'), 'error')\n } finally {\n setRetrying(false)\n }\n }, [failedCount, loadRun, run, t])\n\n const outcomeColumns = React.useMemo<ColumnDef<BillRunOutcome>[]>(\n () => [\n {\n accessorKey: 'bill_account_id',\n header: t('billing.runs.outcomes.columns.account', 'Account'),\n cell: ({ row }) => (\n <Link\n href={`/backend/billing/accounts/${row.original.bill_account_id}`}\n className=\"text-sm text-primary hover:underline\"\n >\n {row.original.bill_account_name ?? row.original.bill_account_id}\n </Link>\n ),\n },\n {\n accessorKey: 'status',\n header: t('billing.runs.outcomes.columns.status', 'Status'),\n cell: ({ row }) => (\n <Tag variant={outcomeStatusVariant(row.original.status)}>\n {row.original.status}\n </Tag>\n ),\n },\n {\n accessorKey: 'draft_invoice_id',\n header: t('billing.runs.outcomes.columns.invoice', 'Invoice'),\n cell: ({ row }) =>\n row.original.draft_invoice_id ? (\n <Link\n href={`/backend/billing/invoices/${row.original.draft_invoice_id}`}\n className=\"text-sm text-primary hover:underline\"\n >\n {row.original.invoice_number ?? row.original.draft_invoice_id}\n </Link>\n ) : (\n '\u2014'\n ),\n },\n {\n accessorKey: 'error_message',\n header: t('billing.runs.outcomes.columns.error', 'Error'),\n cell: ({ row }) => row.original.error_message ?? '\u2014',\n },\n {\n accessorKey: 'warnings',\n header: t('billing.runs.outcomes.columns.warnings', 'Warnings'),\n cell: ({ row }) => {\n if (!row.original.warnings) return '\u2014'\n return (\n <span className=\"font-mono text-xs\">{JSON.stringify(row.original.warnings)}</span>\n )\n },\n },\n ],\n [t],\n )\n\n if (runError) {\n return (\n <Page>\n <PageHeader title={t('billing.runs.detail.title', 'Bill Run')} />\n <PageBody>\n <ErrorMessage label={runError} />\n </PageBody>\n </Page>\n )\n }\n\n if (!run) {\n return (\n <Page>\n <PageHeader title={t('billing.runs.detail.title', 'Bill Run')} />\n <PageBody>\n <LoadingMessage label={t('billing.runs.detail.loading', 'Loading run\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/billing/runs\"\n entityTypeLabel={t('billing.runs.detail.title', 'Bill Run')}\n title={billRunName(run.as_of_date)}\n statusBadge={\n <Tag variant={runStatusVariant(run.status)}>{run.status}</Tag>\n }\n actionsContent={\n failedCount > 0 ? (\n <Button disabled={retrying} onClick={handleRetryFailed}>\n {retrying\n ? t('billing.runs.retry.in_progress', 'Retrying\u2026')\n : t('billing.runs.retry.action', 'Retry failed accounts')}\n {` (${failedCount})`}\n </Button>\n ) : null\n }\n />\n <PageBody>\n <div className=\"space-y-6\">\n <DetailCard>\n <DetailField label={t('billing.runs.detail.field.run_id', 'Run ID')}>\n <span className=\"font-mono text-xs\">{run.id}</span>\n </DetailField>\n <DetailField label={t('billing.runs.detail.field.triggered_by', 'Trigger')}>\n {run.triggered_by}\n </DetailField>\n <DetailField label={t('billing.runs.detail.field.mode', 'Mode')}>\n <div className=\"flex flex-wrap gap-1\">\n {run.dry_run ? (\n <Tag variant=\"warning\">{t('billing.runs.mode.dry_run', 'Dry-run')}</Tag>\n ) : run.test_mode ? (\n <Tag variant=\"warning\">{t('billing.runs.mode.test', 'Test')}</Tag>\n ) : (\n <Tag variant=\"default\">{t('billing.runs.mode.real', 'Real')}</Tag>\n )}\n {run.catch_up ? (\n <Tag variant=\"default\">{t('billing.runs.mode.catch_up', 'Catch-up')}</Tag>\n ) : null}\n </div>\n </DetailField>\n <DetailField label={t('billing.runs.detail.field.as_of_date', 'As-of date')}>\n {run.as_of_date?.slice(0, 10) ?? '\u2014'}\n </DetailField>\n <DetailField label={t('billing.runs.detail.field.started_at', 'Started')}>\n {formatDate(run.started_at)}\n </DetailField>\n <DetailField label={t('billing.runs.detail.field.finished_at', 'Finished')}>\n {formatDate(run.finished_at)}\n </DetailField>\n {run.parent_run_id ? (\n <DetailField label={t('billing.runs.detail.field.parent_run', 'Parent run')}>\n <span className=\"font-mono text-xs\">{run.parent_run_id}</span>\n </DetailField>\n ) : null}\n </DetailCard>\n\n {run.summary ? (\n <StatCard title={t('billing.runs.summary.title', 'Summary')}>\n <Stat\n label={t('billing.runs.summary.accounts', 'Accounts')}\n value={run.summary.accounts_processed ?? 0}\n />\n <Stat\n label={t('billing.runs.summary.drafts', 'Drafts')}\n value={run.summary.drafts_created ?? 0}\n />\n <Stat\n label={t('billing.runs.summary.skipped', 'Skipped')}\n value={run.summary.drafts_skipped_existing ?? 0}\n />\n <Stat\n label={t('billing.runs.summary.warnings', 'Warnings')}\n value={run.summary.accounts_with_warnings ?? 0}\n />\n <Stat\n label={t('billing.runs.summary.failed', 'Failed')}\n value={run.summary.accounts_failed ?? 0}\n />\n <Stat\n label={t('billing.runs.summary.usage', 'Usage rated')}\n value={run.summary.usage_records_rated ?? 0}\n />\n </StatCard>\n ) : null}\n\n <div>\n <h2 className=\"text-base font-semibold mb-2\">\n {t('billing.runs.outcomes.title', 'Per-account outcomes')}\n </h2>\n <DataTable\n columns={outcomeColumns}\n data={outcomes}\n isLoading={outcomesLoading}\n pagination={{\n page: outcomesPage,\n pageSize: 50,\n total: outcomesTotal,\n totalPages: outcomesTotalPages,\n onPageChange: setOutcomesPage,\n }}\n />\n </div>\n </div>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useParams } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\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 { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport {\n apiCallOrThrow,\n readApiResultOrThrow,\n} from '@open-mercato/ui/backend/utils/apiCall'\nimport { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { billRunName } from '../../../../lib/billRunLabel'\nimport {\n DetailCard,\n DetailField,\n Stat,\n StatCard,\n} from '../../../../components/DetailFields'\n\n/**\n * Bill Run detail \u2014 header card + outcomes list.\n *\n * The \"Retry failed accounts\" button is enabled only when the run\n * has at least one failed outcome. It POSTs the\n * `billing.runs.retry_failed` endpoint, which creates a new run\n * scoped to those accounts (the new run shows up in the list at\n * `/backend/billing/runs`).\n */\n\n// API list rows are snake_case (see `api/runs/route.ts` +\n// `api/run-outcomes/route.ts` `fields`).\ntype BillRun = {\n id: string\n triggered_by: 'schedule' | 'manual'\n parent_run_id: string | null\n dry_run: boolean\n test_mode: boolean\n catch_up: boolean\n as_of_date: string\n started_at: string | null\n finished_at: string | null\n status: 'running' | 'completed' | 'partial_failure' | 'failed'\n summary: {\n accounts_processed?: number\n drafts_created?: number\n drafts_skipped_existing?: number\n accounts_failed?: number\n accounts_with_warnings?: number\n usage_records_rated?: number\n } | null\n organization_id: string\n tenant_id: string\n created_at: string\n updated_at: string\n}\n\ntype BillRunOutcome = {\n id: string\n bill_run_id: string\n bill_account_id: string\n bill_account_name: string | null\n status: 'success' | 'success_with_warnings' | 'skipped_existing_draft' | 'failed'\n error_message: string | null\n warnings: Record<string, unknown> | null\n draft_invoice_id: string | null\n invoice_number: string | null\n created_at: string\n}\n\ntype ListResponse<T> = {\n items: T[]\n total: number\n totalPages: number\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString()\n}\n\nfunction runStatusVariant(\n status: BillRun['status'],\n): 'default' | 'success' | 'warning' | 'error' {\n switch (status) {\n case 'completed':\n return 'success'\n case 'partial_failure':\n return 'warning'\n case 'failed':\n return 'error'\n default:\n return 'default'\n }\n}\n\nfunction outcomeStatusVariant(\n status: BillRunOutcome['status'],\n): 'default' | 'success' | 'warning' | 'error' {\n switch (status) {\n case 'success':\n return 'success'\n case 'success_with_warnings':\n return 'warning'\n case 'failed':\n return 'error'\n case 'skipped_existing_draft':\n default:\n return 'default'\n }\n}\n\nexport default function BillRunDetailPage(props: { params?: { id?: string } }) {\n const t = useT()\n // OM serves backend pages through a catch-all route \u2014 the dynamic\n // `[id]` segment arrives as a page prop; `useParams()` is the fallback.\n const urlParams = useParams<{ id: string }>()\n const runId = (props.params?.id ?? urlParams?.id ?? '') as string\n\n const [run, setRun] = React.useState<BillRun | null>(null)\n const [runError, setRunError] = React.useState<string | null>(null)\n const [outcomes, setOutcomes] = React.useState<BillRunOutcome[]>([])\n const [outcomesPage, setOutcomesPage] = React.useState(1)\n const [outcomesTotal, setOutcomesTotal] = React.useState(0)\n const [outcomesTotalPages, setOutcomesTotalPages] = React.useState(1)\n const [outcomesLoading, setOutcomesLoading] = React.useState(true)\n const [retrying, setRetrying] = React.useState(false)\n\n const loadRun = React.useCallback(async () => {\n if (!runId) return\n setRunError(null)\n try {\n const result = await readApiResultOrThrow<ListResponse<BillRun>>(\n `/api/billing/runs?id=${runId}&pageSize=1`,\n )\n const found = result.items?.[0]\n if (!found) {\n setRunError(t('billing.runs.detail.error.not_found', 'Run not found'))\n setRun(null)\n return\n }\n setRun(found)\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n setRunError(message || t('billing.runs.detail.error.load', 'Failed to load run'))\n }\n }, [runId, t])\n\n const loadOutcomes = React.useCallback(async () => {\n if (!runId) return\n setOutcomesLoading(true)\n try {\n const params = new URLSearchParams({\n billRunId: runId,\n page: String(outcomesPage),\n pageSize: '50',\n })\n const result = await readApiResultOrThrow<ListResponse<BillRunOutcome>>(\n `/api/billing/run-outcomes?${params.toString()}`,\n )\n setOutcomes(result.items ?? [])\n setOutcomesTotal(result.total ?? 0)\n setOutcomesTotalPages(result.totalPages ?? 1)\n } finally {\n setOutcomesLoading(false)\n }\n }, [outcomesPage, runId])\n\n React.useEffect(() => {\n void loadRun()\n }, [loadRun])\n React.useEffect(() => {\n void loadOutcomes()\n }, [loadOutcomes])\n\n const failedCount = run?.summary?.accounts_failed ?? 0\n\n const handleRetryFailed = React.useCallback(async () => {\n if (!run || failedCount === 0) return\n setRetrying(true)\n try {\n const call = await apiCallOrThrow<{ billRunId: string; outcomesCount: number }>(\n '/api/billing/runs/retry-failed',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ billRunId: run.id }),\n },\n )\n const outcomesCount = call.result?.outcomesCount ?? 0\n flash(\n t('billing.runs.retry.success', 'Retry run created') +\n ` (${outcomesCount} accounts)`,\n 'success',\n )\n // Refresh \u2014 the new run shows up in the list view; the current\n // run's outcomes are immutable so no reload of `loadOutcomes`\n // needed, but loadRun is cheap.\n void loadRun()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.runs.retry.error', 'Retry failed'), 'error')\n } finally {\n setRetrying(false)\n }\n }, [failedCount, loadRun, run, t])\n\n const outcomeColumns = React.useMemo<ColumnDef<BillRunOutcome>[]>(\n () => [\n {\n accessorKey: 'bill_account_id',\n header: t('billing.runs.outcomes.columns.account', 'Account'),\n cell: ({ row }) => (\n <Link\n href={`/backend/billing/accounts/${row.original.bill_account_id}`}\n className=\"text-sm text-primary hover:underline\"\n >\n {row.original.bill_account_name ?? row.original.bill_account_id}\n </Link>\n ),\n },\n {\n accessorKey: 'status',\n header: t('billing.runs.outcomes.columns.status', 'Status'),\n cell: ({ row }) => (\n <Tag variant={outcomeStatusVariant(row.original.status)}>\n {row.original.status}\n </Tag>\n ),\n },\n {\n accessorKey: 'draft_invoice_id',\n header: t('billing.runs.outcomes.columns.invoice', 'Invoice'),\n cell: ({ row }) =>\n row.original.draft_invoice_id ? (\n <Link\n href={`/backend/billing/invoices/${row.original.draft_invoice_id}`}\n className=\"text-sm text-primary hover:underline\"\n >\n {row.original.invoice_number ?? row.original.draft_invoice_id}\n </Link>\n ) : (\n '\u2014'\n ),\n },\n {\n accessorKey: 'error_message',\n header: t('billing.runs.outcomes.columns.error', 'Error'),\n cell: ({ row }) => row.original.error_message ?? '\u2014',\n },\n {\n accessorKey: 'warnings',\n header: t('billing.runs.outcomes.columns.warnings', 'Warnings'),\n cell: ({ row }) => {\n if (!row.original.warnings) return '\u2014'\n return (\n <span className=\"font-mono text-xs\">{JSON.stringify(row.original.warnings)}</span>\n )\n },\n },\n ],\n [t],\n )\n\n if (runError) {\n return (\n <Page>\n <PageHeader title={t('billing.runs.detail.title', 'Bill Run')} />\n <PageBody>\n <ErrorMessage label={runError} />\n </PageBody>\n </Page>\n )\n }\n\n if (!run) {\n return (\n <Page>\n <PageHeader title={t('billing.runs.detail.title', 'Bill Run')} />\n <PageBody>\n <LoadingMessage label={t('billing.runs.detail.loading', 'Loading run\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/billing/runs\"\n entityTypeLabel={t('billing.runs.detail.title', 'Bill Run')}\n title={billRunName(run.as_of_date)}\n statusBadge={\n <Tag variant={runStatusVariant(run.status)}>{run.status}</Tag>\n }\n actionsContent={\n failedCount > 0 ? (\n <Button disabled={retrying} onClick={handleRetryFailed}>\n {retrying\n ? t('billing.runs.retry.in_progress', 'Retrying\u2026')\n : t('billing.runs.retry.action', 'Retry failed accounts')}\n {` (${failedCount})`}\n </Button>\n ) : null\n }\n />\n <PageBody>\n <div className=\"space-y-6\">\n <DetailCard>\n <DetailField label={t('billing.runs.detail.field.run_id', 'Run ID')}>\n <span className=\"font-mono text-xs\">{run.id}</span>\n </DetailField>\n <DetailField label={t('billing.runs.detail.field.triggered_by', 'Trigger')}>\n {run.triggered_by}\n </DetailField>\n <DetailField label={t('billing.runs.detail.field.mode', 'Mode')}>\n <div className=\"flex flex-wrap gap-1\">\n {run.dry_run ? (\n <Tag variant=\"warning\">{t('billing.runs.mode.dry_run', 'Dry-run')}</Tag>\n ) : run.test_mode ? (\n <Tag variant=\"warning\">{t('billing.runs.mode.test', 'Test')}</Tag>\n ) : (\n <Tag variant=\"default\">{t('billing.runs.mode.real', 'Real')}</Tag>\n )}\n {run.catch_up ? (\n <Tag variant=\"default\">{t('billing.runs.mode.catch_up', 'Catch-up')}</Tag>\n ) : null}\n </div>\n </DetailField>\n <DetailField label={t('billing.runs.detail.field.as_of_date', 'As-of date')}>\n {run.as_of_date?.slice(0, 10) ?? '\u2014'}\n </DetailField>\n <DetailField label={t('billing.runs.detail.field.started_at', 'Started')}>\n {formatDate(run.started_at)}\n </DetailField>\n <DetailField label={t('billing.runs.detail.field.finished_at', 'Finished')}>\n {formatDate(run.finished_at)}\n </DetailField>\n {run.parent_run_id ? (\n <DetailField label={t('billing.runs.detail.field.parent_run', 'Parent run')}>\n <span className=\"font-mono text-xs\">{run.parent_run_id}</span>\n </DetailField>\n ) : null}\n </DetailCard>\n\n {run.summary ? (\n <StatCard title={t('billing.runs.summary.title', 'Summary')}>\n <Stat\n label={t('billing.runs.summary.accounts', 'Accounts')}\n value={run.summary.accounts_processed ?? 0}\n />\n <Stat\n label={t('billing.runs.summary.drafts', 'Drafts')}\n value={run.summary.drafts_created ?? 0}\n />\n <Stat\n label={t('billing.runs.summary.skipped', 'Skipped')}\n value={run.summary.drafts_skipped_existing ?? 0}\n />\n <Stat\n label={t('billing.runs.summary.warnings', 'Warnings')}\n value={run.summary.accounts_with_warnings ?? 0}\n />\n <Stat\n label={t('billing.runs.summary.failed', 'Failed')}\n value={run.summary.accounts_failed ?? 0}\n />\n <Stat\n label={t('billing.runs.summary.usage', 'Usage rated')}\n value={run.summary.usage_records_rated ?? 0}\n />\n </StatCard>\n ) : null}\n\n <div>\n <h2 className=\"text-base font-semibold mb-2\">\n {t('billing.runs.outcomes.title', 'Per-account outcomes')}\n </h2>\n <DataTable\n columns={outcomeColumns}\n data={outcomes}\n isLoading={outcomesLoading}\n pagination={{\n page: outcomesPage,\n pageSize: 50,\n total: outcomesTotal,\n totalPages: outcomesTotalPages,\n onPageChange: setOutcomesPage,\n }}\n />\n </div>\n </div>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/billing/backend/billing/runs/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Play } from 'lucide-react'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\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 { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Kbd, KbdShortcut } from '@open-mercato/ui/primitives/kbd'\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 { billRunName } from '../../../lib/billRunLabel'\n\n/**\n * Bill Runs admin list page.\n *\n * Lists every Bill Run for the operator's tenant with status, mode,\n * summary stats, and a link into the detail page. The \"New run\" button\n * opens an inline trigger dialog that posts to `/api/billing/runs` \u2014\n * same endpoint operators previously had to curl by hand. Retry-failed\n * stays on the run detail page (scoped to a specific run's failures).\n */\n\ntype TriggerMode = 'real' | 'dry' | 'test'\n\nfunction todayIso(): string {\n return new Date().toISOString().slice(0, 10)\n}\n\n// API list rows are snake_case (see `api/runs/route.ts` `fields`).\ntype BillRunRow = {\n id: string\n triggered_by: 'schedule' | 'manual'\n parent_run_id: string | null\n dry_run: boolean\n test_mode: boolean\n catch_up: boolean\n as_of_date: string\n started_at: string | null\n finished_at: string | null\n status: 'running' | 'completed' | 'partial_failure' | 'failed'\n summary: {\n accounts_processed?: number\n drafts_created?: number\n drafts_skipped_existing?: number\n accounts_failed?: number\n accounts_with_warnings?: number\n usage_records_rated?: number\n } | null\n created_at: string\n updated_at: string\n}\n\ntype ListResponse = {\n items: BillRunRow[]\n total: number\n totalPages: number\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString()\n}\n\nfunction formatDateOnly(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()\n}\n\nfunction statusVariant(\n status: BillRunRow['status'],\n): 'default' | 'success' | 'warning' | 'error' {\n switch (status) {\n case 'completed':\n return 'success'\n case 'partial_failure':\n return 'warning'\n case 'failed':\n return 'error'\n case 'running':\n default:\n return 'default'\n }\n}\n\nfunction modeBadge(row: BillRunRow): { label: string; variant: 'default' | 'warning' } | null {\n if (row.dry_run) return { label: 'Dry-run', variant: 'warning' }\n if (row.test_mode) return { label: 'Test', variant: 'warning' }\n return null\n}\n\nexport default function BillRunsListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<BillRunRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const [search, setSearch] = React.useState('')\n\n // Trigger dialog \u2014 controls the New-run modal and its submit state.\n const [triggerOpen, setTriggerOpen] = React.useState(false)\n const [triggerMode, setTriggerMode] = React.useState<TriggerMode>('real')\n const [triggerAsOfDate, setTriggerAsOfDate] = React.useState<string>(todayIso())\n const [triggering, setTriggering] = React.useState(false)\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('billing.runs.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'running', label: t('billing.runs.status.running', 'Running') },\n { value: 'completed', label: t('billing.runs.status.completed', 'Completed') },\n {\n value: 'partial_failure',\n label: t('billing.runs.status.partial_failure', 'Partial failure'),\n },\n { value: 'failed', label: t('billing.runs.status.failed', 'Failed') },\n ],\n },\n {\n id: 'triggeredBy',\n label: t('billing.runs.filters.triggered_by', 'Trigger'),\n type: 'select',\n options: [\n { value: 'schedule', label: t('billing.runs.trigger.schedule', 'Schedule') },\n { value: 'manual', label: t('billing.runs.trigger.manual', 'Manual') },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'startedAt',\n sortDir: 'desc',\n })\n if (search.trim()) params.set('search', search.trim())\n if (typeof filters.status === 'string' && filters.status) {\n params.set('status', filters.status)\n }\n if (typeof filters.triggeredBy === 'string' && filters.triggeredBy) {\n params.set('triggeredBy', filters.triggeredBy)\n }\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/runs?${params.toString()}`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [filters.status, filters.triggeredBy, page, pageSize, search])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const handleTrigger = React.useCallback(async () => {\n setTriggering(true)\n try {\n const payload: Record<string, unknown> = { mode: triggerMode }\n if (triggerAsOfDate) payload.asOfDate = triggerAsOfDate\n const call = await apiCallOrThrow<{ billRunId: string | null }>(\n '/api/billing/runs',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n },\n )\n flash(t('billing.runs.trigger.success', 'Bill Run triggered'), 'success')\n setTriggerOpen(false)\n const newId = call.result?.billRunId\n if (newId) {\n router.push(`/backend/billing/runs/${newId}`)\n } else {\n void loadRows()\n }\n } catch (err) {\n const { message, fieldErrors } = normalizeCrudServerError(err)\n const fieldDetail =\n fieldErrors && Object.keys(fieldErrors).length\n ? Object.entries(fieldErrors)\n .map(([field, msg]) => `${field}: ${msg}`)\n .join('; ')\n : null\n const detailed =\n fieldDetail && message ? `${message} \u2014 ${fieldDetail}` : fieldDetail || message\n flash(detailed || t('billing.runs.trigger.error', 'Failed to trigger run'), 'error')\n } finally {\n setTriggering(false)\n }\n }, [loadRows, router, t, triggerAsOfDate, triggerMode])\n\n const handleTriggerKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n if (!triggering) void handleTrigger()\n }\n },\n [handleTrigger, triggering],\n )\n\n const columns = React.useMemo<ColumnDef<BillRunRow>[]>(\n () => [\n {\n id: 'name',\n header: t('billing.runs.columns.name', 'Bill Run'),\n cell: ({ row }) => (\n <span className=\"text-sm font-medium text-primary\">\n {billRunName(row.original.as_of_date)}\n </span>\n ),\n },\n {\n accessorKey: 'status',\n header: t('billing.runs.columns.status', 'Status'),\n cell: ({ row }) => {\n const mode = modeBadge(row.original)\n return (\n <div className=\"flex items-center gap-2\">\n <Tag variant={statusVariant(row.original.status)}>\n {row.original.status}\n </Tag>\n {mode ? <Tag variant={mode.variant}>{mode.label}</Tag> : null}\n </div>\n )\n },\n },\n {\n accessorKey: 'as_of_date',\n header: t('billing.runs.columns.as_of_date', 'As-of date'),\n cell: ({ row }) => formatDateOnly(row.original.as_of_date),\n },\n {\n accessorKey: 'triggered_by',\n header: t('billing.runs.columns.triggered_by', 'Trigger'),\n },\n {\n id: 'drafts_created',\n header: t('billing.runs.columns.drafts', 'Drafts'),\n cell: ({ row }) => row.original.summary?.drafts_created ?? '\u2014',\n },\n {\n id: 'accounts_processed',\n header: t('billing.runs.columns.accounts', 'Accounts'),\n cell: ({ row }) => row.original.summary?.accounts_processed ?? '\u2014',\n },\n {\n id: 'accounts_failed',\n header: t('billing.runs.columns.failed', 'Failed'),\n cell: ({ row }) => {\n const failed = row.original.summary?.accounts_failed ?? 0\n if (failed === 0) return '\u2014'\n return <Tag variant=\"error\">{failed}</Tag>\n },\n },\n {\n accessorKey: 'started_at',\n header: t('billing.runs.columns.started_at', 'Started'),\n cell: ({ row }) => formatDate(row.original.started_at),\n },\n {\n accessorKey: 'finished_at',\n header: t('billing.runs.columns.finished_at', 'Finished'),\n cell: ({ row }) => formatDate(row.original.finished_at),\n },\n ],\n [t],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.runs.title', 'Bill Runs')}\n actions={\n <Button onClick={() => setTriggerOpen(true)}>\n <Play size={16} />\n {t('billing.runs.trigger.action', 'New run')}\n </Button>\n }\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-runs' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/runs/${row.id}`)}\n isLoading={loading}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n searchValue={search}\n onSearchChange={setSearch}\n />\n\n <Dialog open={triggerOpen} onOpenChange={(next) => !triggering && setTriggerOpen(next)}>\n <DialogContent onKeyDown={handleTriggerKeyDown}>\n <DialogHeader>\n <DialogTitle>\n {t('billing.runs.trigger.dialog.title', 'Trigger Bill Run')}\n </DialogTitle>\n <DialogDescription>\n {t(\n 'billing.runs.trigger.dialog.description',\n 'Runs the bill cycle for every account whose next bill date has passed. Dry-run / Test produce drafts without posting any invoices.',\n )}\n </DialogDescription>\n </DialogHeader>\n <div className=\"space-y-4\">\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.runs.trigger.dialog.mode', 'Mode')}\n </span>\n <Select\n value={triggerMode}\n onValueChange={(next) => setTriggerMode(next as TriggerMode)}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"real\">\n {t('billing.runs.trigger.dialog.mode.real', 'Real \u2014 create draft invoices')}\n </SelectItem>\n <SelectItem value=\"dry\">\n {t('billing.runs.trigger.dialog.mode.dry', 'Dry-run \u2014 no rows written')}\n </SelectItem>\n <SelectItem value=\"test\">\n {t('billing.runs.trigger.dialog.mode.test', 'Test \u2014 drafts tagged for cleanup')}\n </SelectItem>\n </SelectContent>\n </Select>\n </label>\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.runs.trigger.dialog.as_of_date', 'As-of date')}\n </span>\n <Input\n type=\"date\"\n value={triggerAsOfDate}\n onChange={(event) => setTriggerAsOfDate(event.currentTarget.value)}\n />\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'billing.runs.trigger.dialog.as_of_date.help',\n 'Accounts whose next bill date is on or before this date will be billed.',\n )}\n </span>\n </label>\n </div>\n <DialogFooter className=\"flex items-center justify-between gap-3 sm:justify-between\">\n <span className=\"text-xs text-muted-foreground\">\n <KbdShortcut keys={['Ctrl/\u2318', 'Enter']} />{' '}\n {t('billing.runs.trigger.dialog.submit_hint', 'to run,')} <Kbd>Esc</Kbd>{' '}\n {t('billing.runs.trigger.dialog.cancel_hint', 'to cancel')}\n </span>\n <div className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setTriggerOpen(false)}\n disabled={triggering}\n >\n {t('billing.common.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void handleTrigger()} disabled={triggering}>\n {triggering\n ? t('billing.runs.trigger.dialog.running', 'Running\u2026')\n : t('billing.runs.trigger.dialog.submit', 'Run now')}\n </Button>\n </div>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Play } from 'lucide-react'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\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 { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Kbd, KbdShortcut } from '@open-mercato/ui/primitives/kbd'\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 { billRunName } from '../../../lib/billRunLabel'\n\n/**\n * Bill Runs admin list page.\n *\n * Lists every Bill Run for the operator's tenant with status, mode,\n * summary stats, and a link into the detail page. The \"New run\" button\n * opens an inline trigger dialog that posts to `/api/billing/runs` \u2014\n * same endpoint operators previously had to curl by hand. Retry-failed\n * stays on the run detail page (scoped to a specific run's failures).\n */\n\ntype TriggerMode = 'real' | 'dry' | 'test'\n\nfunction todayIso(): string {\n return new Date().toISOString().slice(0, 10)\n}\n\n// API list rows are snake_case (see `api/runs/route.ts` `fields`).\ntype BillRunRow = {\n id: string\n triggered_by: 'schedule' | 'manual'\n parent_run_id: string | null\n dry_run: boolean\n test_mode: boolean\n catch_up: boolean\n as_of_date: string\n started_at: string | null\n finished_at: string | null\n status: 'running' | 'completed' | 'partial_failure' | 'failed'\n summary: {\n accounts_processed?: number\n drafts_created?: number\n drafts_skipped_existing?: number\n accounts_failed?: number\n accounts_with_warnings?: number\n usage_records_rated?: number\n } | null\n created_at: string\n updated_at: string\n}\n\ntype ListResponse = {\n items: BillRunRow[]\n total: number\n totalPages: number\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString()\n}\n\nfunction formatDateOnly(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()\n}\n\nfunction statusVariant(\n status: BillRunRow['status'],\n): 'default' | 'success' | 'warning' | 'error' {\n switch (status) {\n case 'completed':\n return 'success'\n case 'partial_failure':\n return 'warning'\n case 'failed':\n return 'error'\n case 'running':\n default:\n return 'default'\n }\n}\n\nfunction modeBadge(row: BillRunRow): { label: string; variant: 'default' | 'warning' } | null {\n if (row.dry_run) return { label: 'Dry-run', variant: 'warning' }\n if (row.test_mode) return { label: 'Test', variant: 'warning' }\n return null\n}\n\nexport default function BillRunsListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<BillRunRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const [search, setSearch] = React.useState('')\n\n // Trigger dialog \u2014 controls the New-run modal and its submit state.\n const [triggerOpen, setTriggerOpen] = React.useState(false)\n const [triggerMode, setTriggerMode] = React.useState<TriggerMode>('real')\n const [triggerAsOfDate, setTriggerAsOfDate] = React.useState<string>(todayIso())\n const [triggering, setTriggering] = React.useState(false)\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('billing.runs.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'running', label: t('billing.runs.status.running', 'Running') },\n { value: 'completed', label: t('billing.runs.status.completed', 'Completed') },\n {\n value: 'partial_failure',\n label: t('billing.runs.status.partial_failure', 'Partial failure'),\n },\n { value: 'failed', label: t('billing.runs.status.failed', 'Failed') },\n ],\n },\n {\n id: 'triggeredBy',\n label: t('billing.runs.filters.triggered_by', 'Trigger'),\n type: 'select',\n options: [\n { value: 'schedule', label: t('billing.runs.trigger.schedule', 'Schedule') },\n { value: 'manual', label: t('billing.runs.trigger.manual', 'Manual') },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'startedAt',\n sortDir: 'desc',\n })\n if (search.trim()) params.set('search', search.trim())\n if (typeof filters.status === 'string' && filters.status) {\n params.set('status', filters.status)\n }\n if (typeof filters.triggeredBy === 'string' && filters.triggeredBy) {\n params.set('triggeredBy', filters.triggeredBy)\n }\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/runs?${params.toString()}`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [filters.status, filters.triggeredBy, page, pageSize, search])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const handleTrigger = React.useCallback(async () => {\n setTriggering(true)\n try {\n const payload: Record<string, unknown> = { mode: triggerMode }\n if (triggerAsOfDate) payload.asOfDate = triggerAsOfDate\n const call = await apiCallOrThrow<{ billRunId: string | null }>(\n '/api/billing/runs',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n },\n )\n flash(t('billing.runs.trigger.success', 'Bill Run triggered'), 'success')\n setTriggerOpen(false)\n const newId = call.result?.billRunId\n if (newId) {\n router.push(`/backend/billing/runs/${newId}`)\n } else {\n void loadRows()\n }\n } catch (err) {\n const { message, fieldErrors } = normalizeCrudServerError(err)\n const fieldDetail =\n fieldErrors && Object.keys(fieldErrors).length\n ? Object.entries(fieldErrors)\n .map(([field, msg]) => `${field}: ${msg}`)\n .join('; ')\n : null\n const detailed =\n fieldDetail && message ? `${message} \u2014 ${fieldDetail}` : fieldDetail || message\n flash(detailed || t('billing.runs.trigger.error', 'Failed to trigger run'), 'error')\n } finally {\n setTriggering(false)\n }\n }, [loadRows, router, t, triggerAsOfDate, triggerMode])\n\n const handleTriggerKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n if (!triggering) void handleTrigger()\n }\n },\n [handleTrigger, triggering],\n )\n\n const columns = React.useMemo<ColumnDef<BillRunRow>[]>(\n () => [\n {\n id: 'name',\n header: t('billing.runs.columns.name', 'Bill Run'),\n cell: ({ row }) => (\n <span className=\"text-sm font-medium text-primary\">\n {billRunName(row.original.as_of_date)}\n </span>\n ),\n },\n {\n accessorKey: 'status',\n header: t('billing.runs.columns.status', 'Status'),\n cell: ({ row }) => {\n const mode = modeBadge(row.original)\n return (\n <div className=\"flex items-center gap-2\">\n <Tag variant={statusVariant(row.original.status)}>\n {row.original.status}\n </Tag>\n {mode ? <Tag variant={mode.variant}>{mode.label}</Tag> : null}\n </div>\n )\n },\n },\n {\n accessorKey: 'as_of_date',\n header: t('billing.runs.columns.as_of_date', 'As-of date'),\n cell: ({ row }) => formatDateOnly(row.original.as_of_date),\n },\n {\n accessorKey: 'triggered_by',\n header: t('billing.runs.columns.triggered_by', 'Trigger'),\n },\n {\n id: 'drafts_created',\n header: t('billing.runs.columns.drafts', 'Drafts'),\n cell: ({ row }) => row.original.summary?.drafts_created ?? '\u2014',\n },\n {\n id: 'accounts_processed',\n header: t('billing.runs.columns.accounts', 'Accounts'),\n cell: ({ row }) => row.original.summary?.accounts_processed ?? '\u2014',\n },\n {\n id: 'accounts_failed',\n header: t('billing.runs.columns.failed', 'Failed'),\n cell: ({ row }) => {\n const failed = row.original.summary?.accounts_failed ?? 0\n if (failed === 0) return '\u2014'\n return <Tag variant=\"error\">{failed}</Tag>\n },\n },\n {\n accessorKey: 'started_at',\n header: t('billing.runs.columns.started_at', 'Started'),\n cell: ({ row }) => formatDate(row.original.started_at),\n },\n {\n accessorKey: 'finished_at',\n header: t('billing.runs.columns.finished_at', 'Finished'),\n cell: ({ row }) => formatDate(row.original.finished_at),\n },\n ],\n [t],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.runs.title', 'Bill Runs')}\n actions={\n <Button onClick={() => setTriggerOpen(true)}>\n <Play size={16} />\n {t('billing.runs.trigger.action', 'New run')}\n </Button>\n }\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-runs' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/runs/${row.id}`)}\n isLoading={loading}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n searchValue={search}\n onSearchChange={setSearch}\n />\n\n <Dialog open={triggerOpen} onOpenChange={(next) => !triggering && setTriggerOpen(next)}>\n <DialogContent onKeyDown={handleTriggerKeyDown}>\n <DialogHeader>\n <DialogTitle>\n {t('billing.runs.trigger.dialog.title', 'Trigger Bill Run')}\n </DialogTitle>\n <DialogDescription>\n {t(\n 'billing.runs.trigger.dialog.description',\n 'Runs the bill cycle for every account whose next bill date has passed. Dry-run / Test produce drafts without posting any invoices.',\n )}\n </DialogDescription>\n </DialogHeader>\n <div className=\"space-y-4\">\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.runs.trigger.dialog.mode', 'Mode')}\n </span>\n <Select\n value={triggerMode}\n onValueChange={(next) => setTriggerMode(next as TriggerMode)}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"real\">\n {t('billing.runs.trigger.dialog.mode.real', 'Real \u2014 create draft invoices')}\n </SelectItem>\n <SelectItem value=\"dry\">\n {t('billing.runs.trigger.dialog.mode.dry', 'Dry-run \u2014 no rows written')}\n </SelectItem>\n <SelectItem value=\"test\">\n {t('billing.runs.trigger.dialog.mode.test', 'Test \u2014 drafts tagged for cleanup')}\n </SelectItem>\n </SelectContent>\n </Select>\n </label>\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.runs.trigger.dialog.as_of_date', 'As-of date')}\n </span>\n <Input\n type=\"date\"\n value={triggerAsOfDate}\n onChange={(event) => setTriggerAsOfDate(event.currentTarget.value)}\n />\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'billing.runs.trigger.dialog.as_of_date.help',\n 'Accounts whose next bill date is on or before this date will be billed.',\n )}\n </span>\n </label>\n </div>\n <DialogFooter className=\"flex items-center justify-between gap-3 sm:justify-between\">\n <span className=\"text-xs text-muted-foreground\">\n <KbdShortcut keys={['Ctrl/\u2318', 'Enter']} />{' '}\n {t('billing.runs.trigger.dialog.submit_hint', 'to run,')} <Kbd>Esc</Kbd>{' '}\n {t('billing.runs.trigger.dialog.cancel_hint', 'to cancel')}\n </span>\n <div className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setTriggerOpen(false)}\n disabled={triggering}\n >\n {t('billing.common.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void handleTrigger()} disabled={triggering}>\n {triggering\n ? t('billing.runs.trigger.dialog.running', 'Running\u2026')\n : t('billing.runs.trigger.dialog.submit', 'Run now')}\n </Button>\n </div>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": ";AAoPU,cAWE,YAXF;AAnPV,YAAY,WAAW;AACvB,SAAS,iBAAiB;AAC1B,SAAS,YAAY;AAErB,SAAS,YAAY;AAErB,SAAS,MAAM,gBAAgB;AAC/B,SAAS,iBAAiB;AAC1B,SAAS,WAAW;AACpB,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,KAAK,mBAAmB;AACjC,SAAS,aAAa;AACtB,SAAS,gBAAgB,4BAA4B;AACrD,SAAS,gCAAgC;AACzC,SAAS,mBAAmB;AAc5B,SAAS,WAAmB;AAC1B,UAAO,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAC7C;AAgCA,SAAS,WAAW,OAA0C;AAC5D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,SAAS,IAAI,KAAK,KAAK;AAC7B,SAAO,OAAO,MAAM,OAAO,QAAQ,CAAC,IAAI,QAAQ,OAAO,eAAe;AACxE;AAEA,SAAS,eAAe,OAA0C;AAChE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,SAAS,IAAI,KAAK,KAAK;AAC7B,SAAO,OAAO,MAAM,OAAO,QAAQ,CAAC,IAAI,QAAQ,OAAO,mBAAmB;AAC5E;AAEA,SAAS,cACP,QAC6C;AAC7C,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,UAAU,KAA2E;AAC5F,MAAI,IAAI,QAAS,QAAO,EAAE,OAAO,WAAW,SAAS,UAAU;AAC/D,MAAI,IAAI,UAAW,QAAO,EAAE,OAAO,QAAQ,SAAS,UAAU;AAC9D,SAAO;AACT;AAEe,SAAR,mBAAoC;AACzC,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAuB,CAAC,CAAC;AACvD,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,CAAC;AACxC,QAAM,CAAC,QAAQ,IAAI,MAAM,SAAS,EAAE;AACpC,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAuB,CAAC,CAAC;AAC7D,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,CAAC;AAC1C,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,CAAC;AACpD,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,EAAE;AAG7C,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,KAAK;AAC1D,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAsB,MAAM;AACxE,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAiB,SAAS,CAAC;AAC/E,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AAExD,QAAM,aAAa,MAAM;AAAA,IACvB,MAAM;AAAA,MACJ;AAAA,QACE,IAAI;AAAA,QACJ,OAAO,EAAE,+BAA+B,QAAQ;AAAA,QAChD,MAAM;AAAA,QACN,SAAS;AAAA,UACP,EAAE,OAAO,WAAW,OAAO,EAAE,+BAA+B,SAAS,EAAE;AAAA,UACvE,EAAE,OAAO,aAAa,OAAO,EAAE,iCAAiC,WAAW,EAAE;AAAA,UAC7E;AAAA,YACE,OAAO;AAAA,YACP,OAAO,EAAE,uCAAuC,iBAAiB;AAAA,UACnE;AAAA,UACA,EAAE,OAAO,UAAU,OAAO,EAAE,8BAA8B,QAAQ,EAAE;AAAA,QACtE;AAAA,MACF;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,OAAO,EAAE,qCAAqC,SAAS;AAAA,QACvD,MAAM;AAAA,QACN,SAAS;AAAA,UACP,EAAE,OAAO,YAAY,OAAO,EAAE,iCAAiC,UAAU,EAAE;AAAA,UAC3E,EAAE,OAAO,UAAU,OAAO,EAAE,+BAA+B,QAAQ,EAAE;AAAA,QACvE;AAAA,MACF;AAAA,IACF;AAAA,IACA,CAAC,CAAC;AAAA,EACJ;AAEA,QAAM,WAAW,MAAM,YAAY,YAAY;AAC7C,eAAW,IAAI;AACf,QAAI;AACF,YAAM,SAAS,IAAI,gBAAgB;AAAA,QACjC,MAAM,OAAO,IAAI;AAAA,QACjB,UAAU,OAAO,QAAQ;AAAA,QACzB,WAAW;AAAA,QACX,SAAS;AAAA,MACX,CAAC;AACD,UAAI,OAAO,KAAK,EAAG,QAAO,IAAI,UAAU,OAAO,KAAK,CAAC;AACrD,UAAI,OAAO,QAAQ,WAAW,YAAY,QAAQ,QAAQ;AACxD,eAAO,IAAI,UAAU,QAAQ,MAAM;AAAA,MACrC;AACA,UAAI,OAAO,QAAQ,gBAAgB,YAAY,QAAQ,aAAa;AAClE,eAAO,IAAI,eAAe,QAAQ,WAAW;AAAA,MAC/C;AACA,YAAM,SAAS,MAAM;AAAA,QACnB,qBAAqB,OAAO,SAAS,CAAC;AAAA,MACxC;AACA,cAAQ,OAAO,SAAS,CAAC,CAAC;AAC1B,eAAS,OAAO,SAAS,CAAC;AAC1B,oBAAc,OAAO,cAAc,CAAC;AAAA,IACtC,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,QAAQ,QAAQ,QAAQ,aAAa,MAAM,UAAU,MAAM,CAAC;AAEhE,QAAM,UAAU,MAAM;AACpB,SAAK,SAAS;AAAA,EAChB,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,gBAAgB,MAAM,YAAY,YAAY;AAClD,kBAAc,IAAI;AAClB,QAAI;AACF,YAAM,UAAmC,EAAE,MAAM,YAAY;AAC7D,UAAI,gBAAiB,SAAQ,WAAW;AACxC,YAAM,OAAO,MAAM;AAAA,QACjB;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,QAC9B;AAAA,MACF;AACA,YAAM,EAAE,gCAAgC,oBAAoB,GAAG,SAAS;AACxE,qBAAe,KAAK;AACpB,YAAM,QAAQ,KAAK,QAAQ;AAC3B,UAAI,OAAO;AACT,eAAO,KAAK,yBAAyB,KAAK,EAAE;AAAA,MAC9C,OAAO;AACL,aAAK,SAAS;AAAA,MAChB;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,EAAE,SAAS,YAAY,IAAI,yBAAyB,GAAG;AAC7D,YAAM,cACJ,eAAe,OAAO,KAAK,WAAW,EAAE,SACpC,OAAO,QAAQ,WAAW,EACvB,IAAI,CAAC,CAAC,OAAO,GAAG,MAAM,GAAG,KAAK,KAAK,GAAG,EAAE,EACxC,KAAK,IAAI,IACZ;AACN,YAAM,WACJ,eAAe,UAAU,GAAG,OAAO,WAAM,WAAW,KAAK,eAAe;AAC1E,YAAM,YAAY,EAAE,8BAA8B,uBAAuB,GAAG,OAAO;AAAA,IACrF,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,UAAU,QAAQ,GAAG,iBAAiB,WAAW,CAAC;AAEtD,QAAM,uBAAuB,MAAM;AAAA,IACjC,CAAC,UAA+C;AAC9C,WAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAC7D,cAAM,eAAe;AACrB,YAAI,CAAC,WAAY,MAAK,cAAc;AAAA,MACtC;AAAA,IACF;AAAA,IACA,CAAC,eAAe,UAAU;AAAA,EAC5B;AAEA,QAAM,UAAU,MAAM;AAAA,IACpB,MAAM;AAAA,MACJ;AAAA,QACE,IAAI;AAAA,QACJ,QAAQ,EAAE,6BAA6B,UAAU;AAAA,QACjD,MAAM,CAAC,EAAE,IAAI,MACX,oBAAC,UAAK,WAAU,oCACb,sBAAY,IAAI,SAAS,UAAU,GACtC;AAAA,MAEJ;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,+BAA+B,QAAQ;AAAA,QACjD,MAAM,CAAC,EAAE,IAAI,MAAM;AACjB,gBAAM,OAAO,UAAU,IAAI,QAAQ;AACnC,iBACE,qBAAC,SAAI,WAAU,2BACb;AAAA,gCAAC,OAAI,SAAS,cAAc,IAAI,SAAS,MAAM,GAC5C,cAAI,SAAS,QAChB;AAAA,YACC,OAAO,oBAAC,OAAI,SAAS,KAAK,SAAU,eAAK,OAAM,IAAS;AAAA,aAC3D;AAAA,QAEJ;AAAA,MACF;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,mCAAmC,YAAY;AAAA,QACzD,MAAM,CAAC,EAAE,IAAI,MAAM,eAAe,IAAI,SAAS,UAAU;AAAA,MAC3D;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,qCAAqC,SAAS;AAAA,MAC1D;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,QAAQ,EAAE,+BAA+B,QAAQ;AAAA,QACjD,MAAM,CAAC,EAAE,IAAI,MAAM,IAAI,SAAS,SAAS,kBAAkB;AAAA,MAC7D;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,QAAQ,EAAE,iCAAiC,UAAU;AAAA,QACrD,MAAM,CAAC,EAAE,IAAI,MAAM,IAAI,SAAS,SAAS,sBAAsB;AAAA,MACjE;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,QAAQ,EAAE,+BAA+B,QAAQ;AAAA,QACjD,MAAM,CAAC,EAAE,IAAI,MAAM;AACjB,gBAAM,SAAS,IAAI,SAAS,SAAS,mBAAmB;AACxD,cAAI,WAAW,EAAG,QAAO;AACzB,iBAAO,oBAAC,OAAI,SAAQ,SAAS,kBAAO;AAAA,QACtC;AAAA,MACF;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,mCAAmC,SAAS;AAAA,QACtD,MAAM,CAAC,EAAE,IAAI,MAAM,WAAW,IAAI,SAAS,UAAU;AAAA,MACvD;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,oCAAoC,UAAU;AAAA,QACxD,MAAM,CAAC,EAAE,IAAI,MAAM,WAAW,IAAI,SAAS,WAAW;AAAA,MACxD;AAAA,IACF;AAAA,IACA,CAAC,CAAC;AAAA,EACJ;AAEA,SACE,oBAAC,QACC,+BAAC,YACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,sBAAsB,WAAW;AAAA,QAC1C,SACE,qBAAC,UAAO,SAAS,MAAM,eAAe,IAAI,GACxC;AAAA,8BAAC,QAAK,MAAM,IAAI;AAAA,UACf,EAAE,+BAA+B,SAAS;AAAA,WAC7C;AAAA,QAEF,eAAe;AAAA,UACb,OAAO,EAAE,0BAA0B,SAAS;AAAA,UAC5C,WAAW;AAAA,UACX,cAAc;AAAA,QAChB;AAAA,QACA,eAAe,EAAE,MAAM,KAAK;AAAA,QAC5B,aAAa,EAAE,SAAS,eAAe;AAAA,QACvC;AAAA,QACA,MAAM;AAAA,QACN,YAAY,CAAC,QAAQ,OAAO,KAAK,yBAAyB,IAAI,EAAE,EAAE;AAAA,QAClE,WAAW;AAAA,QACX,YAAY;AAAA,UACV;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,cAAc;AAAA,QAChB;AAAA,QACA,SAAS;AAAA,QACT,cAAc;AAAA,QACd,gBAAgB;AAAA,QAChB,gBAAgB,MAAM,WAAW,CAAC,CAAC;AAAA,QACnC,aAAa;AAAA,QACb,gBAAgB;AAAA;AAAA,IAClB;AAAA,IAEA,oBAAC,UAAO,MAAM,aAAa,cAAc,CAAC,SAAS,CAAC,cAAc,eAAe,IAAI,GACnF,+BAAC,iBAAc,WAAW,sBACxB;AAAA,2BAAC,gBACC;AAAA,4BAAC,eACE,YAAE,qCAAqC,kBAAkB,GAC5D;AAAA,QACA,oBAAC,qBACE;AAAA,UACC;AAAA,UACA;AAAA,QACF,GACF;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,aACb;AAAA,6BAAC,WAAM,WAAU,+BACf;AAAA,8BAAC,UAAK,WAAU,yBACb,YAAE,oCAAoC,MAAM,GAC/C;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,eAAe,CAAC,SAAS,eAAe,IAAmB;AAAA,cAE3D;AAAA,oCAAC,iBACC,8BAAC,eAAY,GACf;AAAA,gBACA,qBAAC,iBACC;AAAA,sCAAC,cAAW,OAAM,QACf,YAAE,yCAAyC,mCAA8B,GAC5E;AAAA,kBACA,oBAAC,cAAW,OAAM,OACf,YAAE,wCAAwC,gCAA2B,GACxE;AAAA,kBACA,oBAAC,cAAW,OAAM,QACf,YAAE,yCAAyC,uCAAkC,GAChF;AAAA,mBACF;AAAA;AAAA;AAAA,UACF;AAAA,WACF;AAAA,QACA,qBAAC,WAAM,WAAU,+BACf;AAAA,8BAAC,UAAK,WAAU,yBACb,YAAE,0CAA0C,YAAY,GAC3D;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,OAAO;AAAA,cACP,UAAU,CAAC,UAAU,mBAAmB,MAAM,cAAc,KAAK;AAAA;AAAA,UACnE;AAAA,UACA,oBAAC,UAAK,WAAU,iCACb;AAAA,YACC;AAAA,YACA;AAAA,UACF,GACF;AAAA,WACF;AAAA,SACF;AAAA,MACA,qBAAC,gBAAa,WAAU,8DACtB;AAAA,6BAAC,UAAK,WAAU,iCACd;AAAA,8BAAC,eAAY,MAAM,CAAC,eAAU,OAAO,GAAG;AAAA,UAAG;AAAA,UAC1C,EAAE,2CAA2C,SAAS;AAAA,UAAE;AAAA,UAAC,oBAAC,OAAI,iBAAG;AAAA,UAAO;AAAA,UACxE,EAAE,2CAA2C,WAAW;AAAA,WAC3D;AAAA,QACA,qBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,SAAS,MAAM,eAAe,KAAK;AAAA,cACnC,UAAU;AAAA,cAET,YAAE,yBAAyB,QAAQ;AAAA;AAAA,UACtC;AAAA,UACA,oBAAC,UAAO,MAAK,UAAS,SAAS,MAAM,KAAK,cAAc,GAAG,UAAU,YAClE,uBACG,EAAE,uCAAuC,eAAU,IACnD,EAAE,sCAAsC,SAAS,GACvD;AAAA,WACF;AAAA,SACF;AAAA,OACF,GACF;AAAA,KACF,GACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/billing/backend/billing/statements/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useParams } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { DetailCard, DetailField } from '../../../../components/DetailFields'\nimport { statementStatusVariant } from '../page'\n\ntype BreakdownEntry = {\n billing_item_id: string\n billing_type: string\n uom_code?: string\n quantity: string\n amount: string\n}\n\ntype StatementRow = {\n id: string\n bill_account_id: string\n bill_run_id: string | null\n period_start: string\n period_end: string\n currency_code: string\n opening_balance: string\n total_topups: string\n total_usage: string\n total_recurring: string\n total_adjustments: string\n closing_balance: string\n debit_breakdown: { items?: BreakdownEntry[] } | null\n status: string\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const d = new Date(value)\n return Number.isNaN(d.getTime()) ? value : d.toLocaleDateString()\n}\n\nexport default function BillingStatementDetailPage(props: { params?: { id?: string } }) {\n const t = useT()\n const urlParams = useParams<{ id: string }>()\n const statementId = (props.params?.id ?? urlParams?.id ?? '') as string\n\n const [row, setRow] = React.useState<StatementRow | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n let cancelled = false\n if (!statementId) return\n setLoading(true)\n readApiResultOrThrow<{ items: StatementRow[] }>(\n `/api/billing/statements?id=${statementId}&pageSize=1`,\n )\n .then((res) => {\n if (cancelled) return\n const found = res.items?.[0]\n if (!found) setError(t('billing.statements.detail.not_found', 'Statement not found'))\n else setRow(found)\n })\n .catch(() => {\n if (!cancelled)\n setError(t('billing.statements.detail.load_error', 'Failed to load statement'))\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [statementId, t])\n\n const breakdown = row?.debit_breakdown?.items ?? []\n const columns = React.useMemo<ColumnDef<BreakdownEntry>[]>(\n () => [\n {\n accessorKey: 'billing_type',\n header: t('billing.statements.breakdown.type', 'Type'),\n cell: ({ row }) => <Tag variant=\"info\">{row.original.billing_type}</Tag>,\n },\n {\n accessorKey: 'billing_item_id',\n header: t('billing.statements.breakdown.item', 'Item'),\n cell: ({ row }) => <span className=\"font-mono text-xs\">{row.original.billing_item_id}</span>,\n },\n {\n accessorKey: 'quantity',\n header: t('billing.statements.breakdown.quantity', 'Qty'),\n cell: ({ row }) => <span className=\"font-mono tabular-nums\">{row.original.quantity}</span>,\n },\n {\n accessorKey: 'amount',\n header: t('billing.statements.breakdown.amount', 'Amount'),\n cell: ({ row }) => <span className=\"font-mono tabular-nums\">{row.original.amount}</span>,\n },\n ],\n [t],\n )\n\n if (error) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage label={error} />\n </PageBody>\n </Page>\n )\n }\n if (loading || !row) {\n return (\n <Page>\n <PageBody>\n <LoadingMessage label={t('billing.statements.detail.loading', 'Loading statement\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/billing/statements\"\n entityTypeLabel={t('billing.statements.detail.title', 'Statement')}\n title={`${formatDate(row.period_start)} \u2013 ${formatDate(row.period_end)}`}\n statusBadge={\n <Tag variant={statementStatusVariant(row.status)}>\n {t(`billing.statements.status.${row.status}`, row.status)}\n </Tag>\n }\n actionsContent={\n <Button asChild variant=\"outline\">\n <Link href={`/backend/billing/accounts/${row.bill_account_id}`}>\n {t('billing.statements.detail.view_account', 'View account')}\n </Link>\n </Button>\n }\n />\n <PageBody>\n <DetailCard>\n <DetailField label={t('billing.statements.columns.opening', 'Opening')}>\n <span className=\"font-mono tabular-nums\">{row.opening_balance}</span>\n </DetailField>\n <DetailField label={t('billing.statements.columns.topups', 'Top-ups')}>\n <span className=\"font-mono tabular-nums\">{row.total_topups}</span>\n </DetailField>\n <DetailField label={t('billing.statements.columns.usage', 'Usage')}>\n <span className=\"font-mono tabular-nums\">{row.total_usage}</span>\n </DetailField>\n <DetailField label={t('billing.statements.columns.recurring', 'Recurring')}>\n <span className=\"font-mono tabular-nums\">{row.total_recurring}</span>\n </DetailField>\n <DetailField label={t('billing.statements.columns.adjustments', 'Adjustments')}>\n <span className=\"font-mono tabular-nums\">{row.total_adjustments}</span>\n </DetailField>\n <DetailField label={t('billing.statements.columns.closing', 'Closing')}>\n <span className=\"font-mono tabular-nums\">\n {row.closing_balance} {row.currency_code}\n </span>\n </DetailField>\n <DetailField label={t('billing.statements.detail.bill_run', 'Bill Run')}>\n {row.bill_run_id ? (\n <Link\n href={`/backend/billing/runs/${row.bill_run_id}`}\n className=\"font-mono text-xs hover:underline\"\n >\n {row.bill_run_id}\n </Link>\n ) : (\n '\u2014'\n )}\n </DetailField>\n </DetailCard>\n <div className=\"mt-6\">\n <DataTable\n title={t('billing.statements.detail.breakdown', 'Period-close charges')}\n columns={columns}\n data={breakdown}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('billing.statements.detail.no_charges', 'No recurring charges this period.')}\n </div>\n }\n />\n </div>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useParams } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { DetailCard, DetailField } from '../../../../components/DetailFields'\nimport { statementStatusVariant } from '../page'\n\ntype BreakdownEntry = {\n billing_item_id: string\n billing_type: string\n uom_code?: string\n quantity: string\n amount: string\n}\n\ntype StatementRow = {\n id: string\n bill_account_id: string\n bill_run_id: string | null\n period_start: string\n period_end: string\n currency_code: string\n opening_balance: string\n total_topups: string\n total_usage: string\n total_recurring: string\n total_adjustments: string\n closing_balance: string\n debit_breakdown: { items?: BreakdownEntry[] } | null\n status: string\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const d = new Date(value)\n return Number.isNaN(d.getTime()) ? value : d.toLocaleDateString()\n}\n\nexport default function BillingStatementDetailPage(props: { params?: { id?: string } }) {\n const t = useT()\n const urlParams = useParams<{ id: string }>()\n const statementId = (props.params?.id ?? urlParams?.id ?? '') as string\n\n const [row, setRow] = React.useState<StatementRow | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n let cancelled = false\n if (!statementId) return\n setLoading(true)\n readApiResultOrThrow<{ items: StatementRow[] }>(\n `/api/billing/statements?id=${statementId}&pageSize=1`,\n )\n .then((res) => {\n if (cancelled) return\n const found = res.items?.[0]\n if (!found) setError(t('billing.statements.detail.not_found', 'Statement not found'))\n else setRow(found)\n })\n .catch(() => {\n if (!cancelled)\n setError(t('billing.statements.detail.load_error', 'Failed to load statement'))\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [statementId, t])\n\n const breakdown = row?.debit_breakdown?.items ?? []\n const columns = React.useMemo<ColumnDef<BreakdownEntry>[]>(\n () => [\n {\n accessorKey: 'billing_type',\n header: t('billing.statements.breakdown.type', 'Type'),\n cell: ({ row }) => <Tag variant=\"info\">{row.original.billing_type}</Tag>,\n },\n {\n accessorKey: 'billing_item_id',\n header: t('billing.statements.breakdown.item', 'Item'),\n cell: ({ row }) => <span className=\"font-mono text-xs\">{row.original.billing_item_id}</span>,\n },\n {\n accessorKey: 'quantity',\n header: t('billing.statements.breakdown.quantity', 'Qty'),\n cell: ({ row }) => <span className=\"font-mono tabular-nums\">{row.original.quantity}</span>,\n },\n {\n accessorKey: 'amount',\n header: t('billing.statements.breakdown.amount', 'Amount'),\n cell: ({ row }) => <span className=\"font-mono tabular-nums\">{row.original.amount}</span>,\n },\n ],\n [t],\n )\n\n if (error) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage label={error} />\n </PageBody>\n </Page>\n )\n }\n if (loading || !row) {\n return (\n <Page>\n <PageBody>\n <LoadingMessage label={t('billing.statements.detail.loading', 'Loading statement\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/billing/statements\"\n entityTypeLabel={t('billing.statements.detail.title', 'Statement')}\n title={`${formatDate(row.period_start)} \u2013 ${formatDate(row.period_end)}`}\n statusBadge={\n <Tag variant={statementStatusVariant(row.status)}>\n {t(`billing.statements.status.${row.status}`, row.status)}\n </Tag>\n }\n actionsContent={\n <Button asChild variant=\"outline\">\n <Link href={`/backend/billing/accounts/${row.bill_account_id}`}>\n {t('billing.statements.detail.view_account', 'View account')}\n </Link>\n </Button>\n }\n />\n <PageBody>\n <DetailCard>\n <DetailField label={t('billing.statements.columns.opening', 'Opening')}>\n <span className=\"font-mono tabular-nums\">{row.opening_balance}</span>\n </DetailField>\n <DetailField label={t('billing.statements.columns.topups', 'Top-ups')}>\n <span className=\"font-mono tabular-nums\">{row.total_topups}</span>\n </DetailField>\n <DetailField label={t('billing.statements.columns.usage', 'Usage')}>\n <span className=\"font-mono tabular-nums\">{row.total_usage}</span>\n </DetailField>\n <DetailField label={t('billing.statements.columns.recurring', 'Recurring')}>\n <span className=\"font-mono tabular-nums\">{row.total_recurring}</span>\n </DetailField>\n <DetailField label={t('billing.statements.columns.adjustments', 'Adjustments')}>\n <span className=\"font-mono tabular-nums\">{row.total_adjustments}</span>\n </DetailField>\n <DetailField label={t('billing.statements.columns.closing', 'Closing')}>\n <span className=\"font-mono tabular-nums\">\n {row.closing_balance} {row.currency_code}\n </span>\n </DetailField>\n <DetailField label={t('billing.statements.detail.bill_run', 'Bill Run')}>\n {row.bill_run_id ? (\n <Link\n href={`/backend/billing/runs/${row.bill_run_id}`}\n className=\"font-mono text-xs hover:underline\"\n >\n {row.bill_run_id}\n </Link>\n ) : (\n '\u2014'\n )}\n </DetailField>\n </DetailCard>\n <div className=\"mt-6\">\n <DataTable\n title={t('billing.statements.detail.breakdown', 'Period-close charges')}\n columns={columns}\n data={breakdown}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('billing.statements.detail.no_charges', 'No recurring charges this period.')}\n </div>\n }\n />\n </div>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": ";AAuF2B,cA8Ef,YA9Ee;AAtF3B,YAAY,WAAW;AACvB,OAAO,UAAU;AACjB,SAAS,iBAAiB;AAE1B,SAAS,YAAY;AACrB,SAAS,MAAM,gBAAgB;AAC/B,SAAS,kBAAkB;AAC3B,SAAS,WAAW;AACpB,SAAS,cAAc;AACvB,SAAS,iBAAiB;AAC1B,SAAS,gBAAgB,oBAAoB;AAC7C,SAAS,4BAA4B;AACrC,SAAS,YAAY,mBAAmB;AACxC,SAAS,8BAA8B;AA2BvC,SAAS,WAAW,OAA0C;AAC5D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,IAAI,IAAI,KAAK,KAAK;AACxB,SAAO,OAAO,MAAM,EAAE,QAAQ,CAAC,IAAI,QAAQ,EAAE,mBAAmB;AAClE;AAEe,SAAR,2BAA4C,OAAqC;AACtF,QAAM,IAAI,KAAK;AACf,QAAM,YAAY,UAA0B;AAC5C,QAAM,cAAe,MAAM,QAAQ,MAAM,WAAW,MAAM;AAE1D,QAAM,CAAC,KAAK,MAAM,IAAI,MAAM,SAA8B,IAAI;AAC9D,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAE5D,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,QAAI,CAAC,YAAa;AAClB,eAAW,IAAI;AACf;AAAA,MACE,8BAA8B,WAAW;AAAA,IAC3C,EACG,KAAK,CAAC,QAAQ;AACb,UAAI,UAAW;AACf,YAAM,QAAQ,IAAI,QAAQ,CAAC;AAC3B,UAAI,CAAC,MAAO,UAAS,EAAE,uCAAuC,qBAAqB,CAAC;AAAA,UAC/E,QAAO,KAAK;AAAA,IACnB,CAAC,EACA,MAAM,MAAM;AACX,UAAI,CAAC;AACH,iBAAS,EAAE,wCAAwC,0BAA0B,CAAC;AAAA,IAClF,CAAC,EACA,QAAQ,MAAM;AACb,UAAI,CAAC,UAAW,YAAW,KAAK;AAAA,IAClC,CAAC;AACH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,aAAa,CAAC,CAAC;AAEnB,QAAM,YAAY,KAAK,iBAAiB,SAAS,CAAC;AAClD,QAAM,UAAU,MAAM;AAAA,IACpB,MAAM;AAAA,MACJ;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,qCAAqC,MAAM;AAAA,QACrD,MAAM,CAAC,EAAE,KAAAA,KAAI,MAAM,oBAAC,OAAI,SAAQ,QAAQ,UAAAA,KAAI,SAAS,cAAa;AAAA,MACpE;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,qCAAqC,MAAM;AAAA,QACrD,MAAM,CAAC,EAAE,KAAAA,KAAI,MAAM,oBAAC,UAAK,WAAU,qBAAqB,UAAAA,KAAI,SAAS,iBAAgB;AAAA,MACvF;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,yCAAyC,KAAK;AAAA,QACxD,MAAM,CAAC,EAAE,KAAAA,KAAI,MAAM,oBAAC,UAAK,WAAU,0BAA0B,UAAAA,KAAI,SAAS,UAAS;AAAA,MACrF;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,uCAAuC,QAAQ;AAAA,QACzD,MAAM,CAAC,EAAE,KAAAA,KAAI,MAAM,oBAAC,UAAK,WAAU,0BAA0B,UAAAA,KAAI,SAAS,QAAO;AAAA,MACnF;AAAA,IACF;AAAA,IACA,CAAC,CAAC;AAAA,EACJ;AAEA,MAAI,OAAO;AACT,WACE,oBAAC,QACC,8BAAC,YACC,8BAAC,gBAAa,OAAO,OAAO,GAC9B,GACF;AAAA,EAEJ;AACA,MAAI,WAAW,CAAC,KAAK;AACnB,WACE,oBAAC,QACC,8BAAC,YACC,8BAAC,kBAAe,OAAO,EAAE,qCAAqC,yBAAoB,GAAG,GACvF,GACF;AAAA,EAEJ;AAEA,SACE,qBAAC,QACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,UAAS;AAAA,QACT,iBAAiB,EAAE,mCAAmC,WAAW;AAAA,QACjE,OAAO,GAAG,WAAW,IAAI,YAAY,CAAC,WAAM,WAAW,IAAI,UAAU,CAAC;AAAA,QACtE,aACE,oBAAC,OAAI,SAAS,uBAAuB,IAAI,MAAM,GAC5C,YAAE,6BAA6B,IAAI,MAAM,IAAI,IAAI,MAAM,GAC1D;AAAA,QAEF,gBACE,oBAAC,UAAO,SAAO,MAAC,SAAQ,WACtB,8BAAC,QAAK,MAAM,6BAA6B,IAAI,eAAe,IACzD,YAAE,0CAA0C,cAAc,GAC7D,GACF;AAAA;AAAA,IAEJ;AAAA,IACA,qBAAC,YACC;AAAA,2BAAC,cACC;AAAA,4BAAC,eAAY,OAAO,EAAE,sCAAsC,SAAS,GACnE,8BAAC,UAAK,WAAU,0BAA0B,cAAI,iBAAgB,GAChE;AAAA,QACA,oBAAC,eAAY,OAAO,EAAE,qCAAqC,SAAS,GAClE,8BAAC,UAAK,WAAU,0BAA0B,cAAI,cAAa,GAC7D;AAAA,QACA,oBAAC,eAAY,OAAO,EAAE,oCAAoC,OAAO,GAC/D,8BAAC,UAAK,WAAU,0BAA0B,cAAI,aAAY,GAC5D;AAAA,QACA,oBAAC,eAAY,OAAO,EAAE,wCAAwC,WAAW,GACvE,8BAAC,UAAK,WAAU,0BAA0B,cAAI,iBAAgB,GAChE;AAAA,QACA,oBAAC,eAAY,OAAO,EAAE,0CAA0C,aAAa,GAC3E,8BAAC,UAAK,WAAU,0BAA0B,cAAI,mBAAkB,GAClE;AAAA,QACA,oBAAC,eAAY,OAAO,EAAE,sCAAsC,SAAS,GACnE,+BAAC,UAAK,WAAU,0BACb;AAAA,cAAI;AAAA,UAAgB;AAAA,UAAE,IAAI;AAAA,WAC7B,GACF;AAAA,QACA,oBAAC,eAAY,OAAO,EAAE,sCAAsC,UAAU,GACnE,cAAI,cACH;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,yBAAyB,IAAI,WAAW;AAAA,YAC9C,WAAU;AAAA,YAET,cAAI;AAAA;AAAA,QACP,IAEA,UAEJ;AAAA,SACF;AAAA,MACA,oBAAC,SAAI,WAAU,QACb;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,uCAAuC,sBAAsB;AAAA,UACtE;AAAA,UACA,MAAM;AAAA,UACN,YACE,oBAAC,SAAI,WAAU,2EACZ,YAAE,wCAAwC,mCAAmC,GAChF;AAAA;AAAA,MAEJ,GACF;AAAA,OACF;AAAA,KACF;AAEJ;",
|
|
6
6
|
"names": ["row"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/billing/backend/billing/statements/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { fetchAccountNames } from '../../../components/accountNames'\n\n/**\n * Consumption statements admin list (SPEC-002 P3). Non-fiscal period summaries\n * for prepaid accounts \u2014 opening / top-ups / usage / recurring / closing.\n */\n\ntype StatementRow = {\n id: string\n bill_account_id: string\n period_start: string\n period_end: string\n currency_code: string\n opening_balance: string\n total_topups: string\n total_usage: string\n total_recurring: string\n closing_balance: string\n status: string\n}\n\ntype ListResponse = { items: StatementRow[]; total: number; totalPages: number }\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const d = new Date(value)\n return Number.isNaN(d.getTime()) ? value : d.toLocaleDateString()\n}\n\nexport function statementStatusVariant(status: string): 'success' | 'info' {\n return status === 'sent' ? 'success' : 'info'\n}\n\nexport default function BillingStatementsListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<StatementRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [accountNames, setAccountNames] = React.useState<Record<string, string>>({})\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('billing.statements.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'generated', label: t('billing.statements.status.generated', 'Generated') },\n { value: 'sent', label: t('billing.statements.status.sent', 'Sent') },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'periodEnd',\n sortDir: 'desc',\n })\n if (typeof filters.status === 'string' && filters.status) params.set('status', filters.status)\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/statements?${params.toString()}`,\n )\n const items = result.items ?? []\n setRows(items)\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n try {\n const names = await fetchAccountNames(items.map((r) => r.bill_account_id))\n setAccountNames((prev) => ({ ...prev, ...names }))\n } catch {\n // Name resolution is best-effort \u2014 fall back to the id in the cell.\n }\n } finally {\n setLoading(false)\n }\n }, [filters.status, page, pageSize])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const columns = React.useMemo<ColumnDef<StatementRow>[]>(\n () => [\n {\n accessorKey: 'period_end',\n header: t('billing.statements.columns.period', 'Period'),\n cell: ({ row }) =>\n `${formatDate(row.original.period_start)} \u2013 ${formatDate(row.original.period_end)}`,\n },\n {\n accessorKey: 'bill_account_id',\n header: t('billing.statements.columns.account', 'Account'),\n cell: ({ row }) => {\n const name = accountNames[row.original.bill_account_id]\n return name ? (\n <span className=\"text-sm\">{name}</span>\n ) : (\n <span className=\"font-mono text-xs\">{row.original.bill_account_id}</span>\n )\n },\n },\n {\n accessorKey: 'opening_balance',\n header: t('billing.statements.columns.opening', 'Opening'),\n cell: ({ row }) => <span className=\"font-mono tabular-nums\">{row.original.opening_balance}</span>,\n },\n {\n accessorKey: 'total_topups',\n header: t('billing.statements.columns.topups', 'Top-ups'),\n cell: ({ row }) => <span className=\"font-mono tabular-nums\">{row.original.total_topups}</span>,\n },\n {\n accessorKey: 'total_usage',\n header: t('billing.statements.columns.usage', 'Usage'),\n cell: ({ row }) => <span className=\"font-mono tabular-nums\">{row.original.total_usage}</span>,\n },\n {\n accessorKey: 'total_recurring',\n header: t('billing.statements.columns.recurring', 'Recurring'),\n cell: ({ row }) => <span className=\"font-mono tabular-nums\">{row.original.total_recurring}</span>,\n },\n {\n accessorKey: 'closing_balance',\n header: t('billing.statements.columns.closing', 'Closing'),\n cell: ({ row }) => (\n <span className=\"font-mono tabular-nums\">\n {row.original.closing_balance} {row.original.currency_code}\n </span>\n ),\n },\n {\n accessorKey: 'status',\n header: t('billing.statements.columns.status', 'Status'),\n cell: ({ row }) => (\n <Tag variant={statementStatusVariant(row.original.status)}>\n {t(`billing.statements.status.${row.original.status}`, row.original.status)}\n </Tag>\n ),\n },\n ],\n [t, accountNames],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.statements.title', 'Statements')}\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-statements' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/statements/${row.id}`)}\n isLoading={loading}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { fetchAccountNames } from '../../../components/accountNames'\n\n/**\n * Consumption statements admin list (SPEC-002 P3). Non-fiscal period summaries\n * for prepaid accounts \u2014 opening / top-ups / usage / recurring / closing.\n */\n\ntype StatementRow = {\n id: string\n bill_account_id: string\n period_start: string\n period_end: string\n currency_code: string\n opening_balance: string\n total_topups: string\n total_usage: string\n total_recurring: string\n closing_balance: string\n status: string\n}\n\ntype ListResponse = { items: StatementRow[]; total: number; totalPages: number }\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const d = new Date(value)\n return Number.isNaN(d.getTime()) ? value : d.toLocaleDateString()\n}\n\nexport function statementStatusVariant(status: string): 'success' | 'info' {\n return status === 'sent' ? 'success' : 'info'\n}\n\nexport default function BillingStatementsListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<StatementRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [accountNames, setAccountNames] = React.useState<Record<string, string>>({})\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('billing.statements.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'generated', label: t('billing.statements.status.generated', 'Generated') },\n { value: 'sent', label: t('billing.statements.status.sent', 'Sent') },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'periodEnd',\n sortDir: 'desc',\n })\n if (typeof filters.status === 'string' && filters.status) params.set('status', filters.status)\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/statements?${params.toString()}`,\n )\n const items = result.items ?? []\n setRows(items)\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n try {\n const names = await fetchAccountNames(items.map((r) => r.bill_account_id))\n setAccountNames((prev) => ({ ...prev, ...names }))\n } catch {\n // Name resolution is best-effort \u2014 fall back to the id in the cell.\n }\n } finally {\n setLoading(false)\n }\n }, [filters.status, page, pageSize])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const columns = React.useMemo<ColumnDef<StatementRow>[]>(\n () => [\n {\n accessorKey: 'period_end',\n header: t('billing.statements.columns.period', 'Period'),\n cell: ({ row }) =>\n `${formatDate(row.original.period_start)} \u2013 ${formatDate(row.original.period_end)}`,\n },\n {\n accessorKey: 'bill_account_id',\n header: t('billing.statements.columns.account', 'Account'),\n cell: ({ row }) => {\n const name = accountNames[row.original.bill_account_id]\n return name ? (\n <span className=\"text-sm\">{name}</span>\n ) : (\n <span className=\"font-mono text-xs\">{row.original.bill_account_id}</span>\n )\n },\n },\n {\n accessorKey: 'opening_balance',\n header: t('billing.statements.columns.opening', 'Opening'),\n cell: ({ row }) => <span className=\"font-mono tabular-nums\">{row.original.opening_balance}</span>,\n },\n {\n accessorKey: 'total_topups',\n header: t('billing.statements.columns.topups', 'Top-ups'),\n cell: ({ row }) => <span className=\"font-mono tabular-nums\">{row.original.total_topups}</span>,\n },\n {\n accessorKey: 'total_usage',\n header: t('billing.statements.columns.usage', 'Usage'),\n cell: ({ row }) => <span className=\"font-mono tabular-nums\">{row.original.total_usage}</span>,\n },\n {\n accessorKey: 'total_recurring',\n header: t('billing.statements.columns.recurring', 'Recurring'),\n cell: ({ row }) => <span className=\"font-mono tabular-nums\">{row.original.total_recurring}</span>,\n },\n {\n accessorKey: 'closing_balance',\n header: t('billing.statements.columns.closing', 'Closing'),\n cell: ({ row }) => (\n <span className=\"font-mono tabular-nums\">\n {row.original.closing_balance} {row.original.currency_code}\n </span>\n ),\n },\n {\n accessorKey: 'status',\n header: t('billing.statements.columns.status', 'Status'),\n cell: ({ row }) => (\n <Tag variant={statementStatusVariant(row.original.status)}>\n {t(`billing.statements.status.${row.original.status}`, row.original.status)}\n </Tag>\n ),\n },\n ],\n [t, accountNames],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.statements.title', 'Statements')}\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-statements' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/statements/${row.id}`)}\n isLoading={loading}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/billing/backend/billing/topups/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { fetchAccountNames } from '../../../components/accountNames'\n\n/**\n * Registered top-ups admin list (SPEC-002 P2). Read-only \u2014 top-ups are\n * initiated from a prepaid account's detail page and progress through the\n * gateway capture lifecycle.\n */\n\ntype TopupRow = {\n id: string\n bill_account_id: string\n status: string\n amount: string\n currency_code: string\n provider_key: string\n receipt_invoice_id: string | null\n created_at: string\n}\n\ntype ListResponse = { items: TopupRow[]; total: number; totalPages: number }\n\nfunction formatDate(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 statusVariant(status: string): 'success' | 'info' | 'error' | 'default' {\n if (status === 'captured') return 'success'\n if (status === 'pending') return 'info'\n if (status === 'failed') return 'error'\n return 'default' // cancelled / expired\n}\n\nexport default function BillingTopupsListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<TopupRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [search, setSearch] = React.useState('')\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const [accountNames, setAccountNames] = React.useState<Record<string, string>>({})\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('billing.topups.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'pending', label: t('billing.topups.status.pending', 'Pending') },\n { value: 'captured', label: t('billing.topups.status.captured', 'Captured') },\n { value: 'failed', label: t('billing.topups.status.failed', 'Failed') },\n { value: 'cancelled', label: t('billing.topups.status.cancelled', 'Cancelled') },\n { value: 'expired', label: t('billing.topups.status.expired', 'Expired') },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'createdAt',\n sortDir: 'desc',\n })\n if (search.trim()) params.set('search', search.trim())\n if (typeof filters.status === 'string' && filters.status) params.set('status', filters.status)\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/topups?${params.toString()}`,\n )\n const items = result.items ?? []\n setRows(items)\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n try {\n const names = await fetchAccountNames(items.map((r) => r.bill_account_id))\n setAccountNames((prev) => ({ ...prev, ...names }))\n } catch {\n // Name resolution is best-effort \u2014 fall back to the id in the cell.\n }\n } finally {\n setLoading(false)\n }\n }, [filters.status, page, pageSize, search])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const columns = React.useMemo<ColumnDef<TopupRow>[]>(\n () => [\n {\n accessorKey: 'created_at',\n header: t('billing.topups.columns.created', 'Created'),\n cell: ({ row }) => formatDate(row.original.created_at),\n },\n {\n accessorKey: 'amount',\n header: t('billing.topups.columns.amount', 'Amount'),\n cell: ({ row }) => (\n <span className=\"font-mono tabular-nums\">\n {row.original.amount} {row.original.currency_code}\n </span>\n ),\n },\n {\n accessorKey: 'provider_key',\n header: t('billing.topups.columns.provider', 'Provider'),\n cell: ({ row }) => <span className=\"font-mono text-xs\">{row.original.provider_key}</span>,\n },\n {\n accessorKey: 'status',\n header: t('billing.topups.columns.status', 'Status'),\n cell: ({ row }) => (\n <Tag variant={statusVariant(row.original.status)}>\n {t(`billing.topups.status.${row.original.status}`, row.original.status)}\n </Tag>\n ),\n },\n {\n accessorKey: 'bill_account_id',\n header: t('billing.topups.columns.account', 'Account'),\n cell: ({ row }) => {\n const name = accountNames[row.original.bill_account_id]\n return name ? (\n <span className=\"text-sm\">{name}</span>\n ) : (\n <span className=\"font-mono text-xs\">{row.original.bill_account_id}</span>\n )\n },\n },\n ],\n [t, accountNames],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.topups.title', 'Top-ups')}\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-topups' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/topups/${row.id}`)}\n isLoading={loading}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n searchValue={search}\n onSearchChange={setSearch}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { fetchAccountNames } from '../../../components/accountNames'\n\n/**\n * Registered top-ups admin list (SPEC-002 P2). Read-only \u2014 top-ups are\n * initiated from a prepaid account's detail page and progress through the\n * gateway capture lifecycle.\n */\n\ntype TopupRow = {\n id: string\n bill_account_id: string\n status: string\n amount: string\n currency_code: string\n provider_key: string\n receipt_invoice_id: string | null\n created_at: string\n}\n\ntype ListResponse = { items: TopupRow[]; total: number; totalPages: number }\n\nfunction formatDate(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 statusVariant(status: string): 'success' | 'info' | 'error' | 'default' {\n if (status === 'captured') return 'success'\n if (status === 'pending') return 'info'\n if (status === 'failed') return 'error'\n return 'default' // cancelled / expired\n}\n\nexport default function BillingTopupsListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<TopupRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [search, setSearch] = React.useState('')\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const [accountNames, setAccountNames] = React.useState<Record<string, string>>({})\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('billing.topups.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'pending', label: t('billing.topups.status.pending', 'Pending') },\n { value: 'captured', label: t('billing.topups.status.captured', 'Captured') },\n { value: 'failed', label: t('billing.topups.status.failed', 'Failed') },\n { value: 'cancelled', label: t('billing.topups.status.cancelled', 'Cancelled') },\n { value: 'expired', label: t('billing.topups.status.expired', 'Expired') },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'createdAt',\n sortDir: 'desc',\n })\n if (search.trim()) params.set('search', search.trim())\n if (typeof filters.status === 'string' && filters.status) params.set('status', filters.status)\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/topups?${params.toString()}`,\n )\n const items = result.items ?? []\n setRows(items)\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n try {\n const names = await fetchAccountNames(items.map((r) => r.bill_account_id))\n setAccountNames((prev) => ({ ...prev, ...names }))\n } catch {\n // Name resolution is best-effort \u2014 fall back to the id in the cell.\n }\n } finally {\n setLoading(false)\n }\n }, [filters.status, page, pageSize, search])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const columns = React.useMemo<ColumnDef<TopupRow>[]>(\n () => [\n {\n accessorKey: 'created_at',\n header: t('billing.topups.columns.created', 'Created'),\n cell: ({ row }) => formatDate(row.original.created_at),\n },\n {\n accessorKey: 'amount',\n header: t('billing.topups.columns.amount', 'Amount'),\n cell: ({ row }) => (\n <span className=\"font-mono tabular-nums\">\n {row.original.amount} {row.original.currency_code}\n </span>\n ),\n },\n {\n accessorKey: 'provider_key',\n header: t('billing.topups.columns.provider', 'Provider'),\n cell: ({ row }) => <span className=\"font-mono text-xs\">{row.original.provider_key}</span>,\n },\n {\n accessorKey: 'status',\n header: t('billing.topups.columns.status', 'Status'),\n cell: ({ row }) => (\n <Tag variant={statusVariant(row.original.status)}>\n {t(`billing.topups.status.${row.original.status}`, row.original.status)}\n </Tag>\n ),\n },\n {\n accessorKey: 'bill_account_id',\n header: t('billing.topups.columns.account', 'Account'),\n cell: ({ row }) => {\n const name = accountNames[row.original.bill_account_id]\n return name ? (\n <span className=\"text-sm\">{name}</span>\n ) : (\n <span className=\"font-mono text-xs\">{row.original.bill_account_id}</span>\n )\n },\n },\n ],\n [t, accountNames],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.topups.title', 'Top-ups')}\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-topups' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/topups/${row.id}`)}\n isLoading={loading}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n searchValue={search}\n onSearchChange={setSearch}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/billing/backend/billing/transactions/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { fetchAccountNames } from '../../../components/accountNames'\n\n/**\n * Balance transactions admin list (SPEC-002). Read-only \u2014 the append-only\n * ledger is written by the command bus (real-time consume, top-up capture,\n * period close, manual adjustment). Each row links to its detail page so an\n * operator can trace a movement back to its source document.\n */\n\ntype TransactionRow = {\n id: string\n bill_account_id: string\n type: string\n amount: string\n currency_code: string\n balance_after: string\n description: string\n created_at: string\n}\n\ntype ListResponse = { items: TransactionRow[]; total: number; totalPages: number }\n\nfunction formatDate(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 txnVariant(type: string): 'success' | 'info' | 'error' | 'neutral' | 'default' {\n if (type === 'topup' || type === 'adjustment') return 'success'\n if (type === 'usage') return 'info'\n if (type === 'reversal') return 'error'\n return 'neutral' // recurring / one_time\n}\n\nexport default function BillingTransactionsListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<TransactionRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const [accountNames, setAccountNames] = React.useState<Record<string, string>>({})\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'type',\n label: t('billing.transactions.filters.type', 'Type'),\n type: 'select',\n options: [\n { value: 'topup', label: t('billing.transactions.type.topup', 'Top-up') },\n { value: 'usage', label: t('billing.transactions.type.usage', 'Usage') },\n { value: 'recurring', label: t('billing.transactions.type.recurring', 'Recurring') },\n { value: 'one_time', label: t('billing.transactions.type.one_time', 'One-time') },\n { value: 'adjustment', label: t('billing.transactions.type.adjustment', 'Adjustment') },\n { value: 'reversal', label: t('billing.transactions.type.reversal', 'Reversal') },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'createdAt',\n sortDir: 'desc',\n })\n if (typeof filters.type === 'string' && filters.type) params.set('type', filters.type)\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/transactions?${params.toString()}`,\n )\n const items = result.items ?? []\n setRows(items)\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n try {\n const names = await fetchAccountNames(items.map((r) => r.bill_account_id))\n setAccountNames((prev) => ({ ...prev, ...names }))\n } catch {\n // Name resolution is best-effort \u2014 fall back to the id in the cell.\n }\n } finally {\n setLoading(false)\n }\n }, [filters.type, page, pageSize])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\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 }) => formatDate(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\">\n {row.original.amount} {row.original.currency_code}\n </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 accessorKey: 'bill_account_id',\n header: t('billing.transactions.columns.account', 'Account'),\n cell: ({ row }) => {\n const name = accountNames[row.original.bill_account_id]\n return name ? (\n <span className=\"text-sm\">{name}</span>\n ) : (\n <span className=\"font-mono text-xs\">{row.original.bill_account_id}</span>\n )\n },\n },\n ],\n [t, accountNames],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.transactions.title', 'Transactions')}\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-transactions' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/transactions/${row.id}`)}\n isLoading={loading}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { fetchAccountNames } from '../../../components/accountNames'\n\n/**\n * Balance transactions admin list (SPEC-002). Read-only \u2014 the append-only\n * ledger is written by the command bus (real-time consume, top-up capture,\n * period close, manual adjustment). Each row links to its detail page so an\n * operator can trace a movement back to its source document.\n */\n\ntype TransactionRow = {\n id: string\n bill_account_id: string\n type: string\n amount: string\n currency_code: string\n balance_after: string\n description: string\n created_at: string\n}\n\ntype ListResponse = { items: TransactionRow[]; total: number; totalPages: number }\n\nfunction formatDate(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 txnVariant(type: string): 'success' | 'info' | 'error' | 'neutral' | 'default' {\n if (type === 'topup' || type === 'adjustment') return 'success'\n if (type === 'usage') return 'info'\n if (type === 'reversal') return 'error'\n return 'neutral' // recurring / one_time\n}\n\nexport default function BillingTransactionsListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<TransactionRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const [accountNames, setAccountNames] = React.useState<Record<string, string>>({})\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'type',\n label: t('billing.transactions.filters.type', 'Type'),\n type: 'select',\n options: [\n { value: 'topup', label: t('billing.transactions.type.topup', 'Top-up') },\n { value: 'usage', label: t('billing.transactions.type.usage', 'Usage') },\n { value: 'recurring', label: t('billing.transactions.type.recurring', 'Recurring') },\n { value: 'one_time', label: t('billing.transactions.type.one_time', 'One-time') },\n { value: 'adjustment', label: t('billing.transactions.type.adjustment', 'Adjustment') },\n { value: 'reversal', label: t('billing.transactions.type.reversal', 'Reversal') },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'createdAt',\n sortDir: 'desc',\n })\n if (typeof filters.type === 'string' && filters.type) params.set('type', filters.type)\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/transactions?${params.toString()}`,\n )\n const items = result.items ?? []\n setRows(items)\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n try {\n const names = await fetchAccountNames(items.map((r) => r.bill_account_id))\n setAccountNames((prev) => ({ ...prev, ...names }))\n } catch {\n // Name resolution is best-effort \u2014 fall back to the id in the cell.\n }\n } finally {\n setLoading(false)\n }\n }, [filters.type, page, pageSize])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\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 }) => formatDate(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\">\n {row.original.amount} {row.original.currency_code}\n </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 accessorKey: 'bill_account_id',\n header: t('billing.transactions.columns.account', 'Account'),\n cell: ({ row }) => {\n const name = accountNames[row.original.bill_account_id]\n return name ? (\n <span className=\"text-sm\">{name}</span>\n ) : (\n <span className=\"font-mono text-xs\">{row.original.bill_account_id}</span>\n )\n },\n },\n ],\n [t, accountNames],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.transactions.title', 'Transactions')}\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-transactions' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/transactions/${row.id}`)}\n isLoading={loading}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/billing/ce.ts"],
|
|
4
|
-
"sourcesContent": ["import type { CustomEntitySpec } from '@open-mercato/shared/modules/entities'\n\n/**\n * Billing module entity
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { CustomEntitySpec } from '@open-mercato/shared/modules/entities'\n\n/**\n * Billing module custom-entity specs (labels / custom-field surfaces).\n *\n * These ids are real MikroORM tables, not virtual custom entities. The\n * framework only knows that while each id is also in the app's generated\n * `E.billing` registry (fed by the package's shipped\n * `generated/entities.ids.generated.ts`). For an id missing there,\n * `mercato entities install` writes an active `custom_entities` row, and the\n * query engine then reads the id from custom-entity storage instead of its\n * table \u2014 every billing list comes back empty.\n *\n * `fields: []` means billing does NOT expose custom-field surfaces on\n * these entities today (no EAV). Adding fields here is reserved for\n * Phase 4+ if operators request per-tenant custom fields on Billing\n * Account or Billing Item.\n *\n * `showInSidebar: false` keeps the Custom Entities admin UI from listing\n * these \u2014 they are first-class entities with their own dedicated pages\n * (Phase 4), not generic EAV entries.\n */\nconst entities: CustomEntitySpec[] = [\n {\n id: 'billing:billing_account',\n label: 'Billing Account',\n description: 'A per-customer container for billing items, usage, and bill-cycle metadata.',\n labelField: 'name',\n showInSidebar: false,\n fields: [],\n },\n {\n id: 'billing:billing_item',\n label: 'Billing Item',\n description: 'A single charge (one_time / recurring / usage) attached to a Billing Account.',\n labelField: 'description',\n showInSidebar: false,\n fields: [],\n },\n {\n id: 'billing:billing_account_usage',\n label: 'Billing Account Usage',\n description: 'A pre-aggregated usage record uploaded by external metering systems.',\n labelField: 'id',\n showInSidebar: false,\n fields: [],\n },\n {\n id: 'billing:billing_run',\n label: 'Bill Run',\n description: 'A single execution of the Bill Run engine (schedule or manual trigger).',\n labelField: 'id',\n showInSidebar: false,\n fields: [],\n },\n {\n id: 'billing:billing_run_outcome',\n label: 'Bill Run Outcome',\n description: 'Per-account outcome of a Bill Run \u2014 success / warnings / skipped / failed.',\n labelField: 'id',\n showInSidebar: false,\n fields: [],\n },\n {\n id: 'billing:billing_draft_invoice_edit',\n label: 'Draft Invoice Edit',\n description: 'Append-only audit of operator edits on draft invoice lines.',\n labelField: 'id',\n showInSidebar: false,\n fields: [],\n },\n]\n\nexport { entities }\nexport default entities\n"],
|
|
5
|
+
"mappings": "AAsBA,MAAM,WAA+B;AAAA,EACnC;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,QAAQ,CAAC;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,QAAQ,CAAC;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,QAAQ,CAAC;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,QAAQ,CAAC;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,QAAQ,CAAC;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,QAAQ,CAAC;AAAA,EACX;AACF;AAGA,IAAO,aAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "@open-mercato/shared/lib/commands/scope";
|
|
7
7
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
8
8
|
import { SalesInvoice, SalesInvoiceLine } from "@open-mercato/core/modules/sales/data/entities";
|
|
9
|
-
import { BillingAccount,
|
|
9
|
+
import { BillingAccount, BillingDraftInvoiceEdit } from "../data/entities.js";
|
|
10
10
|
import { emitBillingEvent } from "../events.js";
|
|
11
11
|
import { buildPostpaidCreditCrossingEvents } from "../lib/accountStatus.js";
|
|
12
12
|
import {
|
|
@@ -235,7 +235,7 @@ const editDraftLineCommand = {
|
|
|
235
235
|
},
|
|
236
236
|
() => {
|
|
237
237
|
const after = snapshotLine(line);
|
|
238
|
-
audit = em.create(
|
|
238
|
+
audit = em.create(BillingDraftInvoiceEdit, {
|
|
239
239
|
tenantId: parsed.tenantId,
|
|
240
240
|
organizationId: parsed.organizationId,
|
|
241
241
|
invoiceId: invoice.id,
|
|
@@ -305,7 +305,7 @@ const addDraftLineCommand = {
|
|
|
305
305
|
},
|
|
306
306
|
() => {
|
|
307
307
|
const after = snapshotLine(line);
|
|
308
|
-
audit = em.create(
|
|
308
|
+
audit = em.create(BillingDraftInvoiceEdit, {
|
|
309
309
|
tenantId: parsed.tenantId,
|
|
310
310
|
organizationId: parsed.organizationId,
|
|
311
311
|
invoiceId: invoice.id,
|
|
@@ -353,7 +353,7 @@ const removeDraftLineCommand = {
|
|
|
353
353
|
await recomputeInvoiceTotals(em, invoice);
|
|
354
354
|
},
|
|
355
355
|
() => {
|
|
356
|
-
audit = em.create(
|
|
356
|
+
audit = em.create(BillingDraftInvoiceEdit, {
|
|
357
357
|
tenantId: parsed.tenantId,
|
|
358
358
|
organizationId: parsed.organizationId,
|
|
359
359
|
invoiceId: invoice.id,
|