@dainamite/billing 0.17.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 +658 -0
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/modules/billing/acl.js +20 -0
- package/dist/modules/billing/acl.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +182 -0
- package/dist/modules/billing/api/accounts/route.js.map +7 -0
- package/dist/modules/billing/api/export/account/[id]/route.js +170 -0
- package/dist/modules/billing/api/export/account/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/route.js +85 -0
- package/dist/modules/billing/api/invoices/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/add-line/route.js +69 -0
- package/dist/modules/billing/api/invoices/add-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js +78 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/post/route.js +74 -0
- package/dist/modules/billing/api/invoices/post/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/remove-line/route.js +69 -0
- package/dist/modules/billing/api/invoices/remove-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/route.js +109 -0
- package/dist/modules/billing/api/invoices/route.js.map +7 -0
- package/dist/modules/billing/api/items/bulk/route.js +74 -0
- package/dist/modules/billing/api/items/bulk/route.js.map +7 -0
- package/dist/modules/billing/api/items/route.js +224 -0
- package/dist/modules/billing/api/items/route.js.map +7 -0
- package/dist/modules/billing/api/openapi.js +25 -0
- package/dist/modules/billing/api/openapi.js.map +7 -0
- package/dist/modules/billing/api/run-outcomes/route.js +122 -0
- package/dist/modules/billing/api/run-outcomes/route.js.map +7 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js +70 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js.map +7 -0
- package/dist/modules/billing/api/runs/route.js +143 -0
- package/dist/modules/billing/api/runs/route.js.map +7 -0
- package/dist/modules/billing/api/test-invoices/wipe/route.js +65 -0
- package/dist/modules/billing/api/test-invoices/wipe/route.js.map +7 -0
- package/dist/modules/billing/api/usage/route.js +138 -0
- package/dist/modules/billing/api/usage/route.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +402 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js +82 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/page.js +173 -0
- package/dist/modules/billing/backend/billing/accounts/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/accounts/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js +500 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/page.js +187 -0
- package/dist/modules/billing/backend/billing/invoices/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/invoices/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.js +265 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/create/page.js +90 -0
- package/dist/modules/billing/backend/billing/items/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/items/create/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/page.js +208 -0
- package/dist/modules/billing/backend/billing/items/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/items/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js +293 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/page.js +341 -0
- package/dist/modules/billing/backend/billing/runs/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/runs/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/usage/create/page.js +100 -0
- package/dist/modules/billing/backend/billing/usage/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/usage/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/usage/create/page.meta.js.map +7 -0
- package/dist/modules/billing/ce.js +56 -0
- package/dist/modules/billing/ce.js.map +7 -0
- package/dist/modules/billing/cli.js +67 -0
- package/dist/modules/billing/cli.js.map +7 -0
- package/dist/modules/billing/commands/accounts.js +157 -0
- package/dist/modules/billing/commands/accounts.js.map +7 -0
- package/dist/modules/billing/commands/index.js +6 -0
- package/dist/modules/billing/commands/index.js.map +7 -0
- package/dist/modules/billing/commands/invoices.js +401 -0
- package/dist/modules/billing/commands/invoices.js.map +7 -0
- package/dist/modules/billing/commands/items.js +317 -0
- package/dist/modules/billing/commands/items.js.map +7 -0
- package/dist/modules/billing/commands/runs.js +123 -0
- package/dist/modules/billing/commands/runs.js.map +7 -0
- package/dist/modules/billing/commands/usage.js +88 -0
- package/dist/modules/billing/commands/usage.js.map +7 -0
- package/dist/modules/billing/components/AccountForm.js +420 -0
- package/dist/modules/billing/components/AccountForm.js.map +7 -0
- package/dist/modules/billing/components/DetailFields.js +37 -0
- package/dist/modules/billing/components/DetailFields.js.map +7 -0
- package/dist/modules/billing/components/ItemForm.js +440 -0
- package/dist/modules/billing/components/ItemForm.js.map +7 -0
- package/dist/modules/billing/components/LineFormDialog.js +195 -0
- package/dist/modules/billing/components/LineFormDialog.js.map +7 -0
- package/dist/modules/billing/components/UsageForm.js +262 -0
- package/dist/modules/billing/components/UsageForm.js.map +7 -0
- package/dist/modules/billing/data/entities.js +433 -0
- package/dist/modules/billing/data/entities.js.map +7 -0
- package/dist/modules/billing/data/entityIds.js +12 -0
- package/dist/modules/billing/data/entityIds.js.map +7 -0
- package/dist/modules/billing/data/validators.js +396 -0
- package/dist/modules/billing/data/validators.js.map +7 -0
- package/dist/modules/billing/di.js +6 -0
- package/dist/modules/billing/di.js.map +7 -0
- package/dist/modules/billing/events.js +35 -0
- package/dist/modules/billing/events.js.map +7 -0
- package/dist/modules/billing/i18n/en.json +336 -0
- package/dist/modules/billing/i18n/pl.json +336 -0
- package/dist/modules/billing/index.js +17 -0
- package/dist/modules/billing/index.js.map +7 -0
- package/dist/modules/billing/lib/billPeriod.js +101 -0
- package/dist/modules/billing/lib/billPeriod.js.map +7 -0
- package/dist/modules/billing/lib/billRunEngine.js +256 -0
- package/dist/modules/billing/lib/billRunEngine.js.map +7 -0
- package/dist/modules/billing/lib/billRunLabel.js +8 -0
- package/dist/modules/billing/lib/billRunLabel.js.map +7 -0
- package/dist/modules/billing/lib/idempotency.js +13 -0
- package/dist/modules/billing/lib/idempotency.js.map +7 -0
- package/dist/modules/billing/lib/invoiceStatus.js +38 -0
- package/dist/modules/billing/lib/invoiceStatus.js.map +7 -0
- package/dist/modules/billing/lib/invoiceWriter.js +153 -0
- package/dist/modules/billing/lib/invoiceWriter.js.map +7 -0
- package/dist/modules/billing/lib/itemSelector.js +77 -0
- package/dist/modules/billing/lib/itemSelector.js.map +7 -0
- package/dist/modules/billing/lib/seeds/demo.js +130 -0
- package/dist/modules/billing/lib/seeds/demo.js.map +7 -0
- package/dist/modules/billing/lib/tenantLock.js +32 -0
- package/dist/modules/billing/lib/tenantLock.js.map +7 -0
- package/dist/modules/billing/lib/usageRater.js +150 -0
- package/dist/modules/billing/lib/usageRater.js.map +7 -0
- package/dist/modules/billing/lib/usageRunner.js +82 -0
- package/dist/modules/billing/lib/usageRunner.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20260519000000_billing.js +144 -0
- package/dist/modules/billing/migrations/Migration20260519000000_billing.js.map +7 -0
- package/dist/modules/billing/setup.js +190 -0
- package/dist/modules/billing/setup.js.map +7 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js +47 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +7 -0
- package/dist/modules/billing/translations.js +5 -0
- package/dist/modules/billing/translations.js.map +7 -0
- package/dist/modules/billing/workers/reap-stale-bill-runs.js +31 -0
- package/dist/modules/billing/workers/reap-stale-bill-runs.js.map +7 -0
- package/package.json +148 -0
- package/src/modules/billing/__integration__/TC-BILL-001.spec.ts +116 -0
- package/src/modules/billing/__integration__/TC-BILL-002.spec.ts +169 -0
- package/src/modules/billing/__integration__/TC-BILL-003.spec.ts +28 -0
- package/src/modules/billing/__integration__/meta.ts +4 -0
- package/src/modules/billing/__tests__/commands/invoices.test.ts +464 -0
- package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +284 -0
- package/src/modules/billing/__tests__/lib/billPeriod.test.ts +131 -0
- package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +561 -0
- package/src/modules/billing/__tests__/lib/idempotency.test.ts +81 -0
- package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +97 -0
- package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +119 -0
- package/src/modules/billing/__tests__/lib/itemSelector.test.ts +192 -0
- package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +86 -0
- package/src/modules/billing/__tests__/lib/tenantLock.test.ts +126 -0
- package/src/modules/billing/__tests__/lib/usageRater.test.ts +190 -0
- package/src/modules/billing/__tests__/package-isolation.test.ts +223 -0
- package/src/modules/billing/__tests__/setup.test.ts +107 -0
- package/src/modules/billing/__tests__/validators.test.ts +594 -0
- package/src/modules/billing/acl.ts +16 -0
- package/src/modules/billing/api/accounts/route.ts +198 -0
- package/src/modules/billing/api/export/account/[id]/route.ts +233 -0
- package/src/modules/billing/api/invoices/[id]/route.ts +143 -0
- package/src/modules/billing/api/invoices/add-line/route.ts +74 -0
- package/src/modules/billing/api/invoices/edit-line/route.ts +97 -0
- package/src/modules/billing/api/invoices/post/route.ts +97 -0
- package/src/modules/billing/api/invoices/remove-line/route.ts +75 -0
- package/src/modules/billing/api/invoices/route.ts +167 -0
- package/src/modules/billing/api/items/bulk/route.ts +92 -0
- package/src/modules/billing/api/items/route.ts +255 -0
- package/src/modules/billing/api/openapi.ts +33 -0
- package/src/modules/billing/api/run-outcomes/route.ts +149 -0
- package/src/modules/billing/api/runs/retry-failed/route.ts +88 -0
- package/src/modules/billing/api/runs/route.ts +170 -0
- package/src/modules/billing/api/test-invoices/wipe/route.ts +87 -0
- package/src/modules/billing/api/usage/route.ts +148 -0
- package/src/modules/billing/backend/billing/accounts/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +574 -0
- package/src/modules/billing/backend/billing/accounts/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +101 -0
- package/src/modules/billing/backend/billing/accounts/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +222 -0
- package/src/modules/billing/backend/billing/invoices/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +613 -0
- package/src/modules/billing/backend/billing/invoices/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/invoices/page.tsx +245 -0
- package/src/modules/billing/backend/billing/items/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/items/[id]/page.tsx +366 -0
- package/src/modules/billing/backend/billing/items/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/items/create/page.tsx +110 -0
- package/src/modules/billing/backend/billing/items/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/items/page.tsx +272 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.tsx +404 -0
- package/src/modules/billing/backend/billing/runs/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/runs/page.tsx +426 -0
- package/src/modules/billing/backend/billing/usage/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/usage/create/page.tsx +120 -0
- package/src/modules/billing/ce.ts +74 -0
- package/src/modules/billing/cli.ts +89 -0
- package/src/modules/billing/commands/accounts.ts +205 -0
- package/src/modules/billing/commands/index.ts +13 -0
- package/src/modules/billing/commands/invoices.ts +542 -0
- package/src/modules/billing/commands/items.ts +438 -0
- package/src/modules/billing/commands/runs.ts +166 -0
- package/src/modules/billing/commands/usage.ts +118 -0
- package/src/modules/billing/components/AccountForm.tsx +517 -0
- package/src/modules/billing/components/DetailFields.tsx +60 -0
- package/src/modules/billing/components/ItemForm.tsx +558 -0
- package/src/modules/billing/components/LineFormDialog.tsx +253 -0
- package/src/modules/billing/components/UsageForm.tsx +326 -0
- package/src/modules/billing/data/entities.ts +533 -0
- package/src/modules/billing/data/entityIds.ts +25 -0
- package/src/modules/billing/data/validators.ts +562 -0
- package/src/modules/billing/di.ts +14 -0
- package/src/modules/billing/events.ts +47 -0
- package/src/modules/billing/i18n/en.json +336 -0
- package/src/modules/billing/i18n/pl.json +336 -0
- package/src/modules/billing/index.ts +22 -0
- package/src/modules/billing/lib/billPeriod.ts +181 -0
- package/src/modules/billing/lib/billRunEngine.ts +416 -0
- package/src/modules/billing/lib/billRunLabel.ts +14 -0
- package/src/modules/billing/lib/idempotency.ts +46 -0
- package/src/modules/billing/lib/invoiceStatus.ts +61 -0
- package/src/modules/billing/lib/invoiceWriter.ts +260 -0
- package/src/modules/billing/lib/itemSelector.ts +154 -0
- package/src/modules/billing/lib/seeds/demo.ts +199 -0
- package/src/modules/billing/lib/tenantLock.ts +87 -0
- package/src/modules/billing/lib/usageRater.ts +250 -0
- package/src/modules/billing/lib/usageRunner.ts +189 -0
- package/src/modules/billing/migrations/Migration20260519000000_billing.ts +175 -0
- package/src/modules/billing/setup.ts +252 -0
- package/src/modules/billing/subscribers/payment-captured-to-paid.ts +89 -0
- package/src/modules/billing/translations.ts +9 -0
- package/src/modules/billing/workers/reap-stale-bill-runs.ts +57 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { useRouter } from 'next/navigation'
|
|
4
|
+
import { Play } from 'lucide-react'
|
|
5
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
|
|
8
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
9
|
+
import { DataTable } from '@open-mercato/ui/backend/DataTable'
|
|
10
|
+
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
11
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
12
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
13
|
+
import {
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue,
|
|
19
|
+
} from '@open-mercato/ui/primitives/select'
|
|
20
|
+
import {
|
|
21
|
+
Dialog,
|
|
22
|
+
DialogContent,
|
|
23
|
+
DialogDescription,
|
|
24
|
+
DialogFooter,
|
|
25
|
+
DialogHeader,
|
|
26
|
+
DialogTitle,
|
|
27
|
+
} from '@open-mercato/ui/primitives/dialog'
|
|
28
|
+
import { Kbd, KbdShortcut } from '@open-mercato/ui/primitives/kbd'
|
|
29
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
30
|
+
import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
31
|
+
import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
32
|
+
import { billRunName } from '../../../lib/billRunLabel'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Bill Runs admin list page.
|
|
36
|
+
*
|
|
37
|
+
* Lists every Bill Run for the operator's tenant with status, mode,
|
|
38
|
+
* summary stats, and a link into the detail page. The "New run" button
|
|
39
|
+
* opens an inline trigger dialog that posts to `/api/billing/runs` —
|
|
40
|
+
* same endpoint operators previously had to curl by hand. Retry-failed
|
|
41
|
+
* stays on the run detail page (scoped to a specific run's failures).
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
type TriggerMode = 'real' | 'dry' | 'test'
|
|
45
|
+
|
|
46
|
+
function todayIso(): string {
|
|
47
|
+
return new Date().toISOString().slice(0, 10)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// API list rows are snake_case (see `api/runs/route.ts` `fields`).
|
|
51
|
+
type BillRunRow = {
|
|
52
|
+
id: string
|
|
53
|
+
triggered_by: 'schedule' | 'manual'
|
|
54
|
+
parent_run_id: string | null
|
|
55
|
+
dry_run: boolean
|
|
56
|
+
test_mode: boolean
|
|
57
|
+
catch_up: boolean
|
|
58
|
+
as_of_date: string
|
|
59
|
+
started_at: string | null
|
|
60
|
+
finished_at: string | null
|
|
61
|
+
status: 'running' | 'completed' | 'partial_failure' | 'failed'
|
|
62
|
+
summary: {
|
|
63
|
+
accounts_processed?: number
|
|
64
|
+
drafts_created?: number
|
|
65
|
+
drafts_skipped_existing?: number
|
|
66
|
+
accounts_failed?: number
|
|
67
|
+
accounts_with_warnings?: number
|
|
68
|
+
usage_records_rated?: number
|
|
69
|
+
} | null
|
|
70
|
+
created_at: string
|
|
71
|
+
updated_at: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type ListResponse = {
|
|
75
|
+
items: BillRunRow[]
|
|
76
|
+
total: number
|
|
77
|
+
totalPages: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatDate(value: string | null | undefined): string {
|
|
81
|
+
if (!value) return '—'
|
|
82
|
+
const parsed = new Date(value)
|
|
83
|
+
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatDateOnly(value: string | null | undefined): string {
|
|
87
|
+
if (!value) return '—'
|
|
88
|
+
const parsed = new Date(value)
|
|
89
|
+
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function statusVariant(
|
|
93
|
+
status: BillRunRow['status'],
|
|
94
|
+
): 'default' | 'success' | 'warning' | 'error' {
|
|
95
|
+
switch (status) {
|
|
96
|
+
case 'completed':
|
|
97
|
+
return 'success'
|
|
98
|
+
case 'partial_failure':
|
|
99
|
+
return 'warning'
|
|
100
|
+
case 'failed':
|
|
101
|
+
return 'error'
|
|
102
|
+
case 'running':
|
|
103
|
+
default:
|
|
104
|
+
return 'default'
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function modeBadge(row: BillRunRow): { label: string; variant: 'default' | 'warning' } | null {
|
|
109
|
+
if (row.dry_run) return { label: 'Dry-run', variant: 'warning' }
|
|
110
|
+
if (row.test_mode) return { label: 'Test', variant: 'warning' }
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default function BillRunsListPage() {
|
|
115
|
+
const t = useT()
|
|
116
|
+
const router = useRouter()
|
|
117
|
+
const [rows, setRows] = React.useState<BillRunRow[]>([])
|
|
118
|
+
const [page, setPage] = React.useState(1)
|
|
119
|
+
const [pageSize] = React.useState(25)
|
|
120
|
+
const [filters, setFilters] = React.useState<FilterValues>({})
|
|
121
|
+
const [total, setTotal] = React.useState(0)
|
|
122
|
+
const [totalPages, setTotalPages] = React.useState(1)
|
|
123
|
+
const [loading, setLoading] = React.useState(true)
|
|
124
|
+
const [search, setSearch] = React.useState('')
|
|
125
|
+
|
|
126
|
+
// Trigger dialog — controls the New-run modal and its submit state.
|
|
127
|
+
const [triggerOpen, setTriggerOpen] = React.useState(false)
|
|
128
|
+
const [triggerMode, setTriggerMode] = React.useState<TriggerMode>('real')
|
|
129
|
+
const [triggerAsOfDate, setTriggerAsOfDate] = React.useState<string>(todayIso())
|
|
130
|
+
const [triggering, setTriggering] = React.useState(false)
|
|
131
|
+
|
|
132
|
+
const filterDefs = React.useMemo<FilterDef[]>(
|
|
133
|
+
() => [
|
|
134
|
+
{
|
|
135
|
+
id: 'status',
|
|
136
|
+
label: t('billing.runs.filters.status', 'Status'),
|
|
137
|
+
type: 'select',
|
|
138
|
+
options: [
|
|
139
|
+
{ value: 'running', label: t('billing.runs.status.running', 'Running') },
|
|
140
|
+
{ value: 'completed', label: t('billing.runs.status.completed', 'Completed') },
|
|
141
|
+
{
|
|
142
|
+
value: 'partial_failure',
|
|
143
|
+
label: t('billing.runs.status.partial_failure', 'Partial failure'),
|
|
144
|
+
},
|
|
145
|
+
{ value: 'failed', label: t('billing.runs.status.failed', 'Failed') },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: 'triggeredBy',
|
|
150
|
+
label: t('billing.runs.filters.triggered_by', 'Trigger'),
|
|
151
|
+
type: 'select',
|
|
152
|
+
options: [
|
|
153
|
+
{ value: 'schedule', label: t('billing.runs.trigger.schedule', 'Schedule') },
|
|
154
|
+
{ value: 'manual', label: t('billing.runs.trigger.manual', 'Manual') },
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
[t],
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
const loadRows = React.useCallback(async () => {
|
|
162
|
+
setLoading(true)
|
|
163
|
+
try {
|
|
164
|
+
const params = new URLSearchParams({
|
|
165
|
+
page: String(page),
|
|
166
|
+
pageSize: String(pageSize),
|
|
167
|
+
sortField: 'startedAt',
|
|
168
|
+
sortDir: 'desc',
|
|
169
|
+
})
|
|
170
|
+
if (search.trim()) params.set('search', search.trim())
|
|
171
|
+
if (typeof filters.status === 'string' && filters.status) {
|
|
172
|
+
params.set('status', filters.status)
|
|
173
|
+
}
|
|
174
|
+
if (typeof filters.triggeredBy === 'string' && filters.triggeredBy) {
|
|
175
|
+
params.set('triggeredBy', filters.triggeredBy)
|
|
176
|
+
}
|
|
177
|
+
const result = await readApiResultOrThrow<ListResponse>(
|
|
178
|
+
`/api/billing/runs?${params.toString()}`,
|
|
179
|
+
)
|
|
180
|
+
setRows(result.items ?? [])
|
|
181
|
+
setTotal(result.total ?? 0)
|
|
182
|
+
setTotalPages(result.totalPages ?? 1)
|
|
183
|
+
} finally {
|
|
184
|
+
setLoading(false)
|
|
185
|
+
}
|
|
186
|
+
}, [filters.status, filters.triggeredBy, page, pageSize, search])
|
|
187
|
+
|
|
188
|
+
React.useEffect(() => {
|
|
189
|
+
void loadRows()
|
|
190
|
+
}, [loadRows])
|
|
191
|
+
|
|
192
|
+
const handleTrigger = React.useCallback(async () => {
|
|
193
|
+
setTriggering(true)
|
|
194
|
+
try {
|
|
195
|
+
const payload: Record<string, unknown> = { mode: triggerMode }
|
|
196
|
+
if (triggerAsOfDate) payload.asOfDate = triggerAsOfDate
|
|
197
|
+
const call = await apiCallOrThrow<{ billRunId: string | null }>(
|
|
198
|
+
'/api/billing/runs',
|
|
199
|
+
{
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: { 'Content-Type': 'application/json' },
|
|
202
|
+
body: JSON.stringify(payload),
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
flash(t('billing.runs.trigger.success', 'Bill Run triggered'), 'success')
|
|
206
|
+
setTriggerOpen(false)
|
|
207
|
+
const newId = call.result?.billRunId
|
|
208
|
+
if (newId) {
|
|
209
|
+
router.push(`/backend/billing/runs/${newId}`)
|
|
210
|
+
} else {
|
|
211
|
+
void loadRows()
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
const { message, fieldErrors } = normalizeCrudServerError(err)
|
|
215
|
+
const fieldDetail =
|
|
216
|
+
fieldErrors && Object.keys(fieldErrors).length
|
|
217
|
+
? Object.entries(fieldErrors)
|
|
218
|
+
.map(([field, msg]) => `${field}: ${msg}`)
|
|
219
|
+
.join('; ')
|
|
220
|
+
: null
|
|
221
|
+
const detailed =
|
|
222
|
+
fieldDetail && message ? `${message} — ${fieldDetail}` : fieldDetail || message
|
|
223
|
+
flash(detailed || t('billing.runs.trigger.error', 'Failed to trigger run'), 'error')
|
|
224
|
+
} finally {
|
|
225
|
+
setTriggering(false)
|
|
226
|
+
}
|
|
227
|
+
}, [loadRows, router, t, triggerAsOfDate, triggerMode])
|
|
228
|
+
|
|
229
|
+
const handleTriggerKeyDown = React.useCallback(
|
|
230
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
231
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
232
|
+
event.preventDefault()
|
|
233
|
+
if (!triggering) void handleTrigger()
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
[handleTrigger, triggering],
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const columns = React.useMemo<ColumnDef<BillRunRow>[]>(
|
|
240
|
+
() => [
|
|
241
|
+
{
|
|
242
|
+
id: 'name',
|
|
243
|
+
header: t('billing.runs.columns.name', 'Bill Run'),
|
|
244
|
+
cell: ({ row }) => (
|
|
245
|
+
<span className="text-sm font-medium text-primary">
|
|
246
|
+
{billRunName(row.original.as_of_date)}
|
|
247
|
+
</span>
|
|
248
|
+
),
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
accessorKey: 'status',
|
|
252
|
+
header: t('billing.runs.columns.status', 'Status'),
|
|
253
|
+
cell: ({ row }) => {
|
|
254
|
+
const mode = modeBadge(row.original)
|
|
255
|
+
return (
|
|
256
|
+
<div className="flex items-center gap-2">
|
|
257
|
+
<Tag variant={statusVariant(row.original.status)}>
|
|
258
|
+
{row.original.status}
|
|
259
|
+
</Tag>
|
|
260
|
+
{mode ? <Tag variant={mode.variant}>{mode.label}</Tag> : null}
|
|
261
|
+
</div>
|
|
262
|
+
)
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
accessorKey: 'as_of_date',
|
|
267
|
+
header: t('billing.runs.columns.as_of_date', 'As-of date'),
|
|
268
|
+
cell: ({ row }) => formatDateOnly(row.original.as_of_date),
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
accessorKey: 'triggered_by',
|
|
272
|
+
header: t('billing.runs.columns.triggered_by', 'Trigger'),
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: 'drafts_created',
|
|
276
|
+
header: t('billing.runs.columns.drafts', 'Drafts'),
|
|
277
|
+
cell: ({ row }) => row.original.summary?.drafts_created ?? '—',
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: 'accounts_processed',
|
|
281
|
+
header: t('billing.runs.columns.accounts', 'Accounts'),
|
|
282
|
+
cell: ({ row }) => row.original.summary?.accounts_processed ?? '—',
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: 'accounts_failed',
|
|
286
|
+
header: t('billing.runs.columns.failed', 'Failed'),
|
|
287
|
+
cell: ({ row }) => {
|
|
288
|
+
const failed = row.original.summary?.accounts_failed ?? 0
|
|
289
|
+
if (failed === 0) return '—'
|
|
290
|
+
return <Tag variant="error">{failed}</Tag>
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
accessorKey: 'started_at',
|
|
295
|
+
header: t('billing.runs.columns.started_at', 'Started'),
|
|
296
|
+
cell: ({ row }) => formatDate(row.original.started_at),
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
accessorKey: 'finished_at',
|
|
300
|
+
header: t('billing.runs.columns.finished_at', 'Finished'),
|
|
301
|
+
cell: ({ row }) => formatDate(row.original.finished_at),
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
[t],
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<Page>
|
|
309
|
+
<PageBody>
|
|
310
|
+
<DataTable
|
|
311
|
+
title={t('billing.runs.title', 'Bill Runs')}
|
|
312
|
+
actions={
|
|
313
|
+
<Button onClick={() => setTriggerOpen(true)}>
|
|
314
|
+
<Play size={16} />
|
|
315
|
+
{t('billing.runs.trigger.action', 'New run')}
|
|
316
|
+
</Button>
|
|
317
|
+
}
|
|
318
|
+
refreshButton={{
|
|
319
|
+
label: t('billing.common.refresh', 'Refresh'),
|
|
320
|
+
onRefresh: loadRows,
|
|
321
|
+
isRefreshing: loading,
|
|
322
|
+
}}
|
|
323
|
+
columnChooser={{ auto: true }}
|
|
324
|
+
perspective={{ tableId: 'billing-runs' }}
|
|
325
|
+
columns={columns}
|
|
326
|
+
data={rows}
|
|
327
|
+
onRowClick={(row) => router.push(`/backend/billing/runs/${row.id}`)}
|
|
328
|
+
isLoading={loading}
|
|
329
|
+
pagination={{
|
|
330
|
+
page,
|
|
331
|
+
pageSize,
|
|
332
|
+
total,
|
|
333
|
+
totalPages,
|
|
334
|
+
onPageChange: setPage,
|
|
335
|
+
}}
|
|
336
|
+
filters={filterDefs}
|
|
337
|
+
filterValues={filters}
|
|
338
|
+
onFiltersApply={setFilters}
|
|
339
|
+
onFiltersClear={() => setFilters({})}
|
|
340
|
+
searchValue={search}
|
|
341
|
+
onSearchChange={setSearch}
|
|
342
|
+
/>
|
|
343
|
+
|
|
344
|
+
<Dialog open={triggerOpen} onOpenChange={(next) => !triggering && setTriggerOpen(next)}>
|
|
345
|
+
<DialogContent onKeyDown={handleTriggerKeyDown}>
|
|
346
|
+
<DialogHeader>
|
|
347
|
+
<DialogTitle>
|
|
348
|
+
{t('billing.runs.trigger.dialog.title', 'Trigger Bill Run')}
|
|
349
|
+
</DialogTitle>
|
|
350
|
+
<DialogDescription>
|
|
351
|
+
{t(
|
|
352
|
+
'billing.runs.trigger.dialog.description',
|
|
353
|
+
'Runs the bill cycle for every account whose next bill date has passed. Dry-run / Test produce drafts without posting any invoices.',
|
|
354
|
+
)}
|
|
355
|
+
</DialogDescription>
|
|
356
|
+
</DialogHeader>
|
|
357
|
+
<div className="space-y-4">
|
|
358
|
+
<label className="flex flex-col gap-1 text-sm">
|
|
359
|
+
<span className="text-muted-foreground">
|
|
360
|
+
{t('billing.runs.trigger.dialog.mode', 'Mode')}
|
|
361
|
+
</span>
|
|
362
|
+
<Select
|
|
363
|
+
value={triggerMode}
|
|
364
|
+
onValueChange={(next) => setTriggerMode(next as TriggerMode)}
|
|
365
|
+
>
|
|
366
|
+
<SelectTrigger>
|
|
367
|
+
<SelectValue />
|
|
368
|
+
</SelectTrigger>
|
|
369
|
+
<SelectContent>
|
|
370
|
+
<SelectItem value="real">
|
|
371
|
+
{t('billing.runs.trigger.dialog.mode.real', 'Real — create draft invoices')}
|
|
372
|
+
</SelectItem>
|
|
373
|
+
<SelectItem value="dry">
|
|
374
|
+
{t('billing.runs.trigger.dialog.mode.dry', 'Dry-run — no rows written')}
|
|
375
|
+
</SelectItem>
|
|
376
|
+
<SelectItem value="test">
|
|
377
|
+
{t('billing.runs.trigger.dialog.mode.test', 'Test — drafts tagged for cleanup')}
|
|
378
|
+
</SelectItem>
|
|
379
|
+
</SelectContent>
|
|
380
|
+
</Select>
|
|
381
|
+
</label>
|
|
382
|
+
<label className="flex flex-col gap-1 text-sm">
|
|
383
|
+
<span className="text-muted-foreground">
|
|
384
|
+
{t('billing.runs.trigger.dialog.as_of_date', 'As-of date')}
|
|
385
|
+
</span>
|
|
386
|
+
<Input
|
|
387
|
+
type="date"
|
|
388
|
+
value={triggerAsOfDate}
|
|
389
|
+
onChange={(event) => setTriggerAsOfDate(event.currentTarget.value)}
|
|
390
|
+
/>
|
|
391
|
+
<span className="text-xs text-muted-foreground">
|
|
392
|
+
{t(
|
|
393
|
+
'billing.runs.trigger.dialog.as_of_date.help',
|
|
394
|
+
'Accounts whose next bill date is on or before this date will be billed.',
|
|
395
|
+
)}
|
|
396
|
+
</span>
|
|
397
|
+
</label>
|
|
398
|
+
</div>
|
|
399
|
+
<DialogFooter className="flex items-center justify-between gap-3 sm:justify-between">
|
|
400
|
+
<span className="text-xs text-muted-foreground">
|
|
401
|
+
<KbdShortcut keys={['Ctrl/⌘', 'Enter']} />{' '}
|
|
402
|
+
{t('billing.runs.trigger.dialog.submit_hint', 'to run,')} <Kbd>Esc</Kbd>{' '}
|
|
403
|
+
{t('billing.runs.trigger.dialog.cancel_hint', 'to cancel')}
|
|
404
|
+
</span>
|
|
405
|
+
<div className="flex items-center gap-2">
|
|
406
|
+
<Button
|
|
407
|
+
type="button"
|
|
408
|
+
variant="outline"
|
|
409
|
+
onClick={() => setTriggerOpen(false)}
|
|
410
|
+
disabled={triggering}
|
|
411
|
+
>
|
|
412
|
+
{t('billing.common.cancel', 'Cancel')}
|
|
413
|
+
</Button>
|
|
414
|
+
<Button type="button" onClick={() => void handleTrigger()} disabled={triggering}>
|
|
415
|
+
{triggering
|
|
416
|
+
? t('billing.runs.trigger.dialog.running', 'Running…')
|
|
417
|
+
: t('billing.runs.trigger.dialog.submit', 'Run now')}
|
|
418
|
+
</Button>
|
|
419
|
+
</div>
|
|
420
|
+
</DialogFooter>
|
|
421
|
+
</DialogContent>
|
|
422
|
+
</Dialog>
|
|
423
|
+
</PageBody>
|
|
424
|
+
</Page>
|
|
425
|
+
)
|
|
426
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Gauge } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
const usageIcon = React.createElement(Gauge, { size: 16 })
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
requireAuth: true,
|
|
8
|
+
requireFeatures: ['billing.usage.ingest'],
|
|
9
|
+
pageTitle: 'Record Usage',
|
|
10
|
+
pageTitleKey: 'billing.usage.create.title',
|
|
11
|
+
pageGroup: 'Billing',
|
|
12
|
+
pageGroupKey: 'billing.nav.group',
|
|
13
|
+
navHidden: true,
|
|
14
|
+
icon: usageIcon,
|
|
15
|
+
breadcrumb: [
|
|
16
|
+
{
|
|
17
|
+
label: 'Billing Accounts',
|
|
18
|
+
labelKey: 'billing.accounts.title',
|
|
19
|
+
href: '/backend/billing/accounts',
|
|
20
|
+
},
|
|
21
|
+
{ label: 'Record Usage', labelKey: 'billing.usage.create.title' },
|
|
22
|
+
],
|
|
23
|
+
} as const
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'
|
|
6
|
+
import { LoadingMessage } from '@open-mercato/ui/backend/detail'
|
|
7
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
8
|
+
import {
|
|
9
|
+
apiCallOrThrow,
|
|
10
|
+
readApiResultOrThrow,
|
|
11
|
+
} from '@open-mercato/ui/backend/utils/apiCall'
|
|
12
|
+
import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
13
|
+
import {
|
|
14
|
+
UsageForm,
|
|
15
|
+
type UsageAccountOption,
|
|
16
|
+
type AssembledUsagePayload,
|
|
17
|
+
} from '../../../../components/UsageForm'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `/backend/billing/usage/create` — manually record a usage entry.
|
|
21
|
+
*
|
|
22
|
+
* Mirrors `items/create`: loads the account list once to drive the
|
|
23
|
+
* picker, supports `?billAccountId=<id>` deep-link from the account
|
|
24
|
+
* detail page's "Add usage" CTA, and POSTs to `/api/billing/usage`
|
|
25
|
+
* (the same endpoint external meters use). Usage has no detail page —
|
|
26
|
+
* on success we return to the account detail where the new record
|
|
27
|
+
* shows up in the Usage section.
|
|
28
|
+
*/
|
|
29
|
+
export default function CreateBillingUsagePage() {
|
|
30
|
+
const t = useT()
|
|
31
|
+
const router = useRouter()
|
|
32
|
+
const searchParams = useSearchParams()
|
|
33
|
+
const preselectedAccountId = searchParams.get('billAccountId') ?? ''
|
|
34
|
+
|
|
35
|
+
const [accounts, setAccounts] = React.useState<UsageAccountOption[] | null>(null)
|
|
36
|
+
const [submitting, setSubmitting] = React.useState(false)
|
|
37
|
+
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
let cancelled = false
|
|
40
|
+
void (async () => {
|
|
41
|
+
try {
|
|
42
|
+
const result = await readApiResultOrThrow<{ items: UsageAccountOption[] }>(
|
|
43
|
+
'/api/billing/accounts?pageSize=100&sortField=name&sortDir=asc',
|
|
44
|
+
)
|
|
45
|
+
if (!cancelled) setAccounts(result.items ?? [])
|
|
46
|
+
} catch {
|
|
47
|
+
if (!cancelled) setAccounts([])
|
|
48
|
+
}
|
|
49
|
+
})()
|
|
50
|
+
return () => {
|
|
51
|
+
cancelled = true
|
|
52
|
+
}
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
const handleSubmit = React.useCallback(
|
|
56
|
+
async (payload: AssembledUsagePayload) => {
|
|
57
|
+
setSubmitting(true)
|
|
58
|
+
try {
|
|
59
|
+
const call = await apiCallOrThrow<{ id: string; deduplicated: boolean }>(
|
|
60
|
+
'/api/billing/usage',
|
|
61
|
+
{
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify(payload),
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
if (call.result?.deduplicated) {
|
|
68
|
+
flash(
|
|
69
|
+
t(
|
|
70
|
+
'billing.usage.create.deduplicated',
|
|
71
|
+
'A usage record with that source reference already existed — nothing was duplicated.',
|
|
72
|
+
),
|
|
73
|
+
'success',
|
|
74
|
+
)
|
|
75
|
+
} else {
|
|
76
|
+
flash(t('billing.usage.create.success', 'Usage record created'), 'success')
|
|
77
|
+
}
|
|
78
|
+
router.push(`/backend/billing/accounts/${payload.billAccountId}`)
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const { message } = normalizeCrudServerError(err)
|
|
81
|
+
flash(
|
|
82
|
+
message || t('billing.usage.create.error', 'Failed to record usage'),
|
|
83
|
+
'error',
|
|
84
|
+
)
|
|
85
|
+
} finally {
|
|
86
|
+
setSubmitting(false)
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
[router, t],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const handleCancel = React.useCallback(() => {
|
|
93
|
+
if (preselectedAccountId) {
|
|
94
|
+
router.push(`/backend/billing/accounts/${preselectedAccountId}`)
|
|
95
|
+
} else {
|
|
96
|
+
router.push('/backend/billing/accounts')
|
|
97
|
+
}
|
|
98
|
+
}, [preselectedAccountId, router])
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Page>
|
|
102
|
+
<PageHeader title={t('billing.usage.create.title', 'Record Usage')} />
|
|
103
|
+
<PageBody>
|
|
104
|
+
{accounts === null ? (
|
|
105
|
+
<LoadingMessage label={t('billing.usage.create.loading', 'Loading accounts…')} />
|
|
106
|
+
) : (
|
|
107
|
+
<UsageForm
|
|
108
|
+
accountOptions={accounts}
|
|
109
|
+
initial={
|
|
110
|
+
preselectedAccountId ? { billAccountId: preselectedAccountId } : undefined
|
|
111
|
+
}
|
|
112
|
+
submitting={submitting}
|
|
113
|
+
onSubmit={handleSubmit}
|
|
114
|
+
onCancel={handleCancel}
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
117
|
+
</PageBody>
|
|
118
|
+
</Page>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { CustomEntitySpec } from '@open-mercato/shared/modules/entities'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Billing module entity registry.
|
|
5
|
+
*
|
|
6
|
+
* Listing our MikroORM entities here gets them into the framework's
|
|
7
|
+
* `E.billing.<entity>` generated registry (`entities.ids.generated.ts`),
|
|
8
|
+
* which the CRUD factory, query engine, and indexers reference. Phase 0
|
|
9
|
+
* shipped with no entries because the standalone CLI didn't traverse
|
|
10
|
+
* `@dainamite/*` packages — adding them in Phase 1 once the resolver
|
|
11
|
+
* patch lands.
|
|
12
|
+
*
|
|
13
|
+
* `fields: []` means billing does NOT expose custom-field surfaces on
|
|
14
|
+
* these entities today (no EAV). Adding fields here is reserved for
|
|
15
|
+
* Phase 4+ if operators request per-tenant custom fields on Billing
|
|
16
|
+
* Account or Billing Item.
|
|
17
|
+
*
|
|
18
|
+
* `showInSidebar: false` keeps the Custom Entities admin UI from listing
|
|
19
|
+
* these — they are first-class entities with their own dedicated pages
|
|
20
|
+
* (Phase 4), not generic EAV entries.
|
|
21
|
+
*/
|
|
22
|
+
const entities: CustomEntitySpec[] = [
|
|
23
|
+
{
|
|
24
|
+
id: 'billing:billing_account',
|
|
25
|
+
label: 'Billing Account',
|
|
26
|
+
description: 'A per-customer container for billing items, usage, and bill-cycle metadata.',
|
|
27
|
+
labelField: 'name',
|
|
28
|
+
showInSidebar: false,
|
|
29
|
+
fields: [],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'billing:billing_item',
|
|
33
|
+
label: 'Billing Item',
|
|
34
|
+
description: 'A single charge (one_time / recurring / usage) attached to a Billing Account.',
|
|
35
|
+
labelField: 'description',
|
|
36
|
+
showInSidebar: false,
|
|
37
|
+
fields: [],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'billing:billing_account_usage',
|
|
41
|
+
label: 'Billing Account Usage',
|
|
42
|
+
description: 'A pre-aggregated usage record uploaded by external metering systems.',
|
|
43
|
+
labelField: 'id',
|
|
44
|
+
showInSidebar: false,
|
|
45
|
+
fields: [],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'billing:billing_run',
|
|
49
|
+
label: 'Bill Run',
|
|
50
|
+
description: 'A single execution of the Bill Run engine (schedule or manual trigger).',
|
|
51
|
+
labelField: 'id',
|
|
52
|
+
showInSidebar: false,
|
|
53
|
+
fields: [],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'billing:billing_run_outcome',
|
|
57
|
+
label: 'Bill Run Outcome',
|
|
58
|
+
description: 'Per-account outcome of a Bill Run — success / warnings / skipped / failed.',
|
|
59
|
+
labelField: 'id',
|
|
60
|
+
showInSidebar: false,
|
|
61
|
+
fields: [],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'billing:billing_draft_invoice_edit',
|
|
65
|
+
label: 'Draft Invoice Edit',
|
|
66
|
+
description: 'Append-only audit of operator edits on draft invoice lines.',
|
|
67
|
+
labelField: 'id',
|
|
68
|
+
showInSidebar: false,
|
|
69
|
+
fields: [],
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
export { entities }
|
|
74
|
+
export default entities
|