@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,198 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
3
|
+
import { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'
|
|
4
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
5
|
+
import { BillingAccount } from '../../data/entities'
|
|
6
|
+
import { billingEntityIds } from '../../data/entityIds'
|
|
7
|
+
import {
|
|
8
|
+
billingAccountCreateSchema,
|
|
9
|
+
billingAccountListQuerySchema,
|
|
10
|
+
billingAccountUpdateSchema,
|
|
11
|
+
} from '../../data/validators'
|
|
12
|
+
import {
|
|
13
|
+
createBillingCrudOpenApi,
|
|
14
|
+
createPagedListResponseSchema,
|
|
15
|
+
defaultCreateResponseSchema,
|
|
16
|
+
defaultOkResponseSchema,
|
|
17
|
+
} from '../openapi'
|
|
18
|
+
|
|
19
|
+
// ─── Auth / RBAC ─────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const routeMetadata = {
|
|
22
|
+
GET: { requireAuth: true, requireFeatures: ['billing.account.view'] },
|
|
23
|
+
POST: { requireAuth: true, requireFeatures: ['billing.account.manage'] },
|
|
24
|
+
PUT: { requireAuth: true, requireFeatures: ['billing.account.manage'] },
|
|
25
|
+
DELETE: { requireAuth: true, requireFeatures: ['billing.account.manage'] },
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const metadata = routeMetadata
|
|
29
|
+
|
|
30
|
+
// ─── List response item shape (informational — used by OpenAPI) ──
|
|
31
|
+
|
|
32
|
+
// The list endpoint projects raw column names — snake_case.
|
|
33
|
+
const billingAccountListItemSchema = z.object({
|
|
34
|
+
id: z.string().uuid(),
|
|
35
|
+
customer_id: z.string(),
|
|
36
|
+
name: z.string(),
|
|
37
|
+
currency_code: z.string(),
|
|
38
|
+
bill_cycle: z.string(),
|
|
39
|
+
bill_cycle_anchor: z.string(),
|
|
40
|
+
invoice_email: z.string(),
|
|
41
|
+
invoice_language: z.string(),
|
|
42
|
+
tax_id: z.string().nullable(),
|
|
43
|
+
invoice_address: z.record(z.string(), z.unknown()),
|
|
44
|
+
next_bill_date: z.string(),
|
|
45
|
+
last_bill_date: z.string().nullable(),
|
|
46
|
+
is_active: z.boolean(),
|
|
47
|
+
organization_id: z.string().uuid(),
|
|
48
|
+
tenant_id: z.string().uuid(),
|
|
49
|
+
created_at: z.string(),
|
|
50
|
+
updated_at: z.string(),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const rawBodySchema = z.object({}).passthrough()
|
|
54
|
+
|
|
55
|
+
// ─── CRUD route ──────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
const crud = makeCrudRoute({
|
|
58
|
+
metadata: routeMetadata,
|
|
59
|
+
orm: {
|
|
60
|
+
entity: BillingAccount,
|
|
61
|
+
idField: 'id',
|
|
62
|
+
orgField: 'organizationId',
|
|
63
|
+
tenantField: 'tenantId',
|
|
64
|
+
softDeleteField: 'deletedAt',
|
|
65
|
+
},
|
|
66
|
+
list: {
|
|
67
|
+
schema: billingAccountListQuerySchema,
|
|
68
|
+
entityId: billingEntityIds.account,
|
|
69
|
+
fields: [
|
|
70
|
+
'id',
|
|
71
|
+
'customer_id',
|
|
72
|
+
'name',
|
|
73
|
+
'currency_code',
|
|
74
|
+
'bill_cycle',
|
|
75
|
+
'bill_cycle_anchor',
|
|
76
|
+
'invoice_email',
|
|
77
|
+
'invoice_language',
|
|
78
|
+
'tax_id',
|
|
79
|
+
'invoice_address',
|
|
80
|
+
'next_bill_date',
|
|
81
|
+
'last_bill_date',
|
|
82
|
+
'is_active',
|
|
83
|
+
'organization_id',
|
|
84
|
+
'tenant_id',
|
|
85
|
+
'created_at',
|
|
86
|
+
'updated_at',
|
|
87
|
+
],
|
|
88
|
+
sortFieldMap: {
|
|
89
|
+
name: 'name',
|
|
90
|
+
createdAt: 'created_at',
|
|
91
|
+
updatedAt: 'updated_at',
|
|
92
|
+
nextBillDate: 'next_bill_date',
|
|
93
|
+
},
|
|
94
|
+
buildFilters: async (query) => {
|
|
95
|
+
const filters: Record<string, unknown> = {}
|
|
96
|
+
// `?id=<uuid>` narrows the list to one row — the detail page
|
|
97
|
+
// reads a single account through this filter.
|
|
98
|
+
if (typeof query.id === 'string' && query.id) {
|
|
99
|
+
filters.id = { $eq: query.id }
|
|
100
|
+
}
|
|
101
|
+
if (typeof query.customerId === 'string' && query.customerId) {
|
|
102
|
+
filters.customer_id = { $eq: query.customerId }
|
|
103
|
+
}
|
|
104
|
+
if (typeof query.currencyCode === 'string' && query.currencyCode) {
|
|
105
|
+
filters.currency_code = { $eq: query.currencyCode.toUpperCase() }
|
|
106
|
+
}
|
|
107
|
+
if (typeof query.billCycle === 'string' && query.billCycle) {
|
|
108
|
+
filters.bill_cycle = { $eq: query.billCycle }
|
|
109
|
+
}
|
|
110
|
+
if (typeof query.isActive === 'boolean') {
|
|
111
|
+
filters.is_active = { $eq: query.isActive }
|
|
112
|
+
}
|
|
113
|
+
if (typeof query.search === 'string' && query.search.trim()) {
|
|
114
|
+
const pattern = `%${query.search.trim().replace(/[%_]/g, (m) => `\\${m}`)}%`
|
|
115
|
+
filters.name = { $ilike: pattern }
|
|
116
|
+
}
|
|
117
|
+
return filters
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
actions: {
|
|
121
|
+
create: {
|
|
122
|
+
commandId: 'billing.accounts.create',
|
|
123
|
+
schema: rawBodySchema,
|
|
124
|
+
mapInput: async ({ raw, ctx }) => {
|
|
125
|
+
const { translate } = await resolveTranslations()
|
|
126
|
+
const scoped = withScopedPayload(raw ?? {}, ctx, translate)
|
|
127
|
+
return billingAccountCreateSchema.parse(scoped)
|
|
128
|
+
},
|
|
129
|
+
response: ({ result }) => ({ id: result?.id ?? null }),
|
|
130
|
+
status: 201,
|
|
131
|
+
},
|
|
132
|
+
update: {
|
|
133
|
+
commandId: 'billing.accounts.update',
|
|
134
|
+
schema: rawBodySchema,
|
|
135
|
+
mapInput: async ({ raw, ctx }) => {
|
|
136
|
+
const { translate } = await resolveTranslations()
|
|
137
|
+
const scoped = withScopedPayload(raw ?? {}, ctx, translate)
|
|
138
|
+
return billingAccountUpdateSchema.parse(scoped)
|
|
139
|
+
},
|
|
140
|
+
response: () => ({ ok: true }),
|
|
141
|
+
},
|
|
142
|
+
delete: {
|
|
143
|
+
commandId: 'billing.accounts.delete',
|
|
144
|
+
schema: rawBodySchema,
|
|
145
|
+
mapInput: async ({ raw, ctx }) => {
|
|
146
|
+
const { translate } = await resolveTranslations()
|
|
147
|
+
// The factory hands delete a `{ body, query }` envelope — the
|
|
148
|
+
// id may arrive in either, depending on the caller.
|
|
149
|
+
const envelope = (raw ?? {}) as {
|
|
150
|
+
body?: Record<string, unknown>
|
|
151
|
+
query?: Record<string, unknown>
|
|
152
|
+
}
|
|
153
|
+
const body = envelope.body ?? {}
|
|
154
|
+
const query = envelope.query ?? {}
|
|
155
|
+
const scoped = withScopedPayload(body, ctx, translate)
|
|
156
|
+
const id =
|
|
157
|
+
(typeof body.id === 'string' && body.id) ||
|
|
158
|
+
(typeof query.id === 'string' && query.id) ||
|
|
159
|
+
null
|
|
160
|
+
if (!id) {
|
|
161
|
+
throw new Error('id is required')
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
id,
|
|
165
|
+
tenantId: (scoped as { tenantId?: string }).tenantId,
|
|
166
|
+
organizationId: (scoped as { organizationId?: string }).organizationId,
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
response: () => ({ ok: true }),
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const { POST, PUT, DELETE } = crud
|
|
175
|
+
export { POST, PUT, DELETE }
|
|
176
|
+
export const GET = crud.GET
|
|
177
|
+
|
|
178
|
+
export const openApi = createBillingCrudOpenApi({
|
|
179
|
+
resourceName: 'BillingAccount',
|
|
180
|
+
pluralName: 'BillingAccounts',
|
|
181
|
+
querySchema: billingAccountListQuerySchema,
|
|
182
|
+
listResponseSchema: createPagedListResponseSchema(billingAccountListItemSchema),
|
|
183
|
+
create: {
|
|
184
|
+
schema: billingAccountCreateSchema,
|
|
185
|
+
responseSchema: defaultCreateResponseSchema,
|
|
186
|
+
description: 'Creates a new billing account for a customer.',
|
|
187
|
+
},
|
|
188
|
+
update: {
|
|
189
|
+
schema: billingAccountUpdateSchema,
|
|
190
|
+
responseSchema: defaultOkResponseSchema,
|
|
191
|
+
description: 'Updates a billing account. `currencyCode` and `customerId` are immutable.',
|
|
192
|
+
},
|
|
193
|
+
del: {
|
|
194
|
+
schema: z.object({ id: z.string().uuid() }),
|
|
195
|
+
responseSchema: defaultOkResponseSchema,
|
|
196
|
+
description: 'Soft-deletes a billing account (sets `deleted_at`).',
|
|
197
|
+
},
|
|
198
|
+
})
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
5
|
+
import {
|
|
6
|
+
BillingAccount,
|
|
7
|
+
BillingAccountUsage,
|
|
8
|
+
BillingItem,
|
|
9
|
+
BillRunOutcome,
|
|
10
|
+
DraftInvoiceEdit,
|
|
11
|
+
} from '../../../../data/entities'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `GET /api/billing/export/account/[id]` — GDPR portability dump.
|
|
15
|
+
*
|
|
16
|
+
* Returns a full JSON serialization of a single Billing Account
|
|
17
|
+
* including:
|
|
18
|
+
* - The account row itself.
|
|
19
|
+
* - Every active + soft-deleted `BillingItem` for the account.
|
|
20
|
+
* - Every `BillingAccountUsage` record.
|
|
21
|
+
* - Every `BillRunOutcome` row referencing the account.
|
|
22
|
+
* - Every `core/sales` invoice the engine emitted for the account
|
|
23
|
+
* (matched via `metadata->>'bill_account_id'`).
|
|
24
|
+
* - Every `DraftInvoiceEdit` audit row for those invoices.
|
|
25
|
+
*
|
|
26
|
+
* Scope: the caller's auth tenant/organization is the authority — a
|
|
27
|
+
* 404 is returned even for valid UUIDs that belong to a different
|
|
28
|
+
* tenant (no cross-tenant leak).
|
|
29
|
+
*
|
|
30
|
+
* Soft-deleted rows are included on purpose: GDPR portability is
|
|
31
|
+
* about the data we still hold, not just the data the UI shows.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export const metadata = {
|
|
35
|
+
GET: { requireAuth: true, requireFeatures: ['billing.account.view'] },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function GET(
|
|
39
|
+
req: Request,
|
|
40
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
41
|
+
) {
|
|
42
|
+
let container: Awaited<ReturnType<typeof createRequestContainer>> | null = null
|
|
43
|
+
try {
|
|
44
|
+
container = await createRequestContainer()
|
|
45
|
+
const auth = await getAuthFromRequest(req)
|
|
46
|
+
if (!auth || !auth.tenantId) {
|
|
47
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
48
|
+
}
|
|
49
|
+
const tenantId = auth.tenantId
|
|
50
|
+
const organizationId = (auth as { orgId?: string }).orgId ?? null
|
|
51
|
+
if (!organizationId) {
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{ error: 'Organization scope required' },
|
|
54
|
+
{ status: 403 },
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { id: accountId } = await params
|
|
59
|
+
|
|
60
|
+
const em = (container.resolve('em') as EntityManager).fork()
|
|
61
|
+
const account = await em.findOne(BillingAccount, {
|
|
62
|
+
id: accountId,
|
|
63
|
+
tenantId,
|
|
64
|
+
organizationId,
|
|
65
|
+
deletedAt: null,
|
|
66
|
+
})
|
|
67
|
+
if (!account) {
|
|
68
|
+
return NextResponse.json({ error: 'Account not found' }, { status: 404 })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Time-window cap for usage records, run outcomes and invoices.
|
|
72
|
+
//
|
|
73
|
+
// Scale note: a telco account can carry millions of usage rows.
|
|
74
|
+
// Returning everything would OOM on the export path. We default
|
|
75
|
+
// to the last 12 months and let the caller override via `since` /
|
|
76
|
+
// `until` for full GDPR-compliance dumps (a regulator request that
|
|
77
|
+
// legitimately needs the lifetime export should set `since=1970-01-01`).
|
|
78
|
+
// Each query is independently capped by `EXPORT_HARD_LIMIT` so a
|
|
79
|
+
// single year of pathological volume still returns rather than
|
|
80
|
+
// hanging.
|
|
81
|
+
const url = new URL(req.url)
|
|
82
|
+
const since = parseDate(url.searchParams.get('since'))
|
|
83
|
+
?? defaultSince()
|
|
84
|
+
const until = parseDate(url.searchParams.get('until')) ?? new Date()
|
|
85
|
+
|
|
86
|
+
const [items, usage, outcomes] = await Promise.all([
|
|
87
|
+
// Items are bounded by integration volume (typically <100 per
|
|
88
|
+
// account); no time-window needed — they often outlive a single
|
|
89
|
+
// export window and the operator needs to see active items
|
|
90
|
+
// regardless of when they were created.
|
|
91
|
+
em.find(
|
|
92
|
+
BillingItem,
|
|
93
|
+
{ tenantId, organizationId, billAccountId: account.id } as never,
|
|
94
|
+
{ limit: EXPORT_HARD_LIMIT },
|
|
95
|
+
),
|
|
96
|
+
em.find(
|
|
97
|
+
BillingAccountUsage,
|
|
98
|
+
{
|
|
99
|
+
tenantId,
|
|
100
|
+
organizationId,
|
|
101
|
+
billAccountId: account.id,
|
|
102
|
+
periodEnd: { $gte: since, $lte: until },
|
|
103
|
+
} as never,
|
|
104
|
+
{ limit: EXPORT_HARD_LIMIT, orderBy: { periodEnd: 'DESC' } },
|
|
105
|
+
),
|
|
106
|
+
em.find(
|
|
107
|
+
BillRunOutcome,
|
|
108
|
+
{
|
|
109
|
+
tenantId,
|
|
110
|
+
organizationId,
|
|
111
|
+
billAccountId: account.id,
|
|
112
|
+
createdAt: { $gte: since, $lte: until },
|
|
113
|
+
} as never,
|
|
114
|
+
{ limit: EXPORT_HARD_LIMIT, orderBy: { createdAt: 'DESC' } },
|
|
115
|
+
),
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
// Invoices created in the window. The `metadata->>'bill_account_id'`
|
|
119
|
+
// predicate keeps non-billing core/sales invoices out.
|
|
120
|
+
const invoiceRows = (await em.execute(
|
|
121
|
+
`SELECT id, invoice_number, status, status_entry_id, issue_date, due_date,
|
|
122
|
+
currency_code, subtotal_net_amount, grand_total_net_amount,
|
|
123
|
+
grand_total_gross_amount, paid_total_amount, outstanding_amount,
|
|
124
|
+
metadata, created_at, updated_at, deleted_at
|
|
125
|
+
FROM sales_invoices
|
|
126
|
+
WHERE tenant_id = ?
|
|
127
|
+
AND organization_id = ?
|
|
128
|
+
AND deleted_at IS NULL
|
|
129
|
+
AND metadata->>'bill_account_id' = ?
|
|
130
|
+
AND created_at >= ?
|
|
131
|
+
AND created_at <= ?
|
|
132
|
+
ORDER BY created_at DESC
|
|
133
|
+
LIMIT ?`,
|
|
134
|
+
[tenantId, organizationId, account.id, since, until, EXPORT_HARD_LIMIT],
|
|
135
|
+
)) as unknown as Array<{ id: string }>
|
|
136
|
+
|
|
137
|
+
const invoiceIds = invoiceRows.map((r) => r.id)
|
|
138
|
+
const draftEdits =
|
|
139
|
+
invoiceIds.length > 0
|
|
140
|
+
? await em.find(
|
|
141
|
+
DraftInvoiceEdit,
|
|
142
|
+
{
|
|
143
|
+
tenantId,
|
|
144
|
+
organizationId,
|
|
145
|
+
invoiceId: { $in: invoiceIds },
|
|
146
|
+
} as never,
|
|
147
|
+
{ limit: EXPORT_HARD_LIMIT, orderBy: { createdAt: 'DESC' } },
|
|
148
|
+
)
|
|
149
|
+
: []
|
|
150
|
+
|
|
151
|
+
return NextResponse.json({
|
|
152
|
+
exportedAt: new Date().toISOString(),
|
|
153
|
+
schemaVersion: 1,
|
|
154
|
+
tenantId,
|
|
155
|
+
organizationId,
|
|
156
|
+
window: {
|
|
157
|
+
since: since.toISOString(),
|
|
158
|
+
until: until.toISOString(),
|
|
159
|
+
hardLimit: EXPORT_HARD_LIMIT,
|
|
160
|
+
},
|
|
161
|
+
account,
|
|
162
|
+
items,
|
|
163
|
+
usage,
|
|
164
|
+
runOutcomes: outcomes,
|
|
165
|
+
invoices: invoiceRows,
|
|
166
|
+
draftInvoiceEdits: draftEdits,
|
|
167
|
+
})
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error('[billing/export/account.GET]', err)
|
|
170
|
+
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Per-collection cap to keep a single export bounded in size and time
|
|
176
|
+
* even when the caller asks for `since=1970-01-01`. The full GDPR
|
|
177
|
+
* lifetime dump for an outlier account requires a follow-up
|
|
178
|
+
* background-job approach (chunked NDJSON to object storage) — out of
|
|
179
|
+
* scope for the v1 admin export endpoint.
|
|
180
|
+
*/
|
|
181
|
+
const EXPORT_HARD_LIMIT = 50_000
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Default time window for the export — last 12 months. Operators
|
|
185
|
+
* pulling for routine GDPR requests almost always want a finite
|
|
186
|
+
* window; the regulator-compliance "lifetime" case overrides via
|
|
187
|
+
* `?since=1970-01-01`.
|
|
188
|
+
*/
|
|
189
|
+
function defaultSince(): Date {
|
|
190
|
+
const d = new Date()
|
|
191
|
+
d.setUTCFullYear(d.getUTCFullYear() - 1)
|
|
192
|
+
return d
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parseDate(value: string | null | undefined): Date | null {
|
|
196
|
+
if (!value) return null
|
|
197
|
+
const d = new Date(value)
|
|
198
|
+
return Number.isNaN(d.getTime()) ? null : d
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const openApi = {
|
|
202
|
+
methods: {
|
|
203
|
+
GET: {
|
|
204
|
+
tags: ['Billing'],
|
|
205
|
+
summary: 'Export a billing account (GDPR portability)',
|
|
206
|
+
parameters: [
|
|
207
|
+
{
|
|
208
|
+
name: 'id',
|
|
209
|
+
in: 'path',
|
|
210
|
+
required: true,
|
|
211
|
+
schema: { type: 'string', format: 'uuid' },
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'since',
|
|
215
|
+
in: 'query',
|
|
216
|
+
required: false,
|
|
217
|
+
description:
|
|
218
|
+
'ISO timestamp; lower bound for usage / outcomes / invoices windows. Defaults to today − 12 months.',
|
|
219
|
+
schema: { type: 'string', format: 'date-time' },
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'until',
|
|
223
|
+
in: 'query',
|
|
224
|
+
required: false,
|
|
225
|
+
description: 'ISO timestamp; upper bound. Defaults to now.',
|
|
226
|
+
schema: { type: 'string', format: 'date-time' },
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
description:
|
|
230
|
+
"Returns a JSON dump of a single Billing Account: the account itself, every Billing Item (no time cap — items often outlive a single export window), and the Usage records / BillRunOutcomes / emitted core/sales invoices / DraftInvoiceEdit rows whose timestamps fall in the [since, until] window (defaults to the last 12 months). Each collection is independently capped by EXPORT_HARD_LIMIT = 50,000 rows. Lifetime dumps for outlier accounts need a follow-up chunked background-job approach — out of scope for this v1 endpoint.",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `GET /api/billing/invoices/[id]` — billing-invoice detail.
|
|
8
|
+
*
|
|
9
|
+
* Returns the `core/sales` invoice row + its lines, but only if the
|
|
10
|
+
* invoice is billing-managed (carries `metadata.bill_run_id`). Other
|
|
11
|
+
* `core/sales` invoices return 404 here even though the same UUID
|
|
12
|
+
* resolves at `/api/sales/invoices/[id]` — this endpoint is the
|
|
13
|
+
* billing operator's surface, scoped to the billing universe.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const metadata = {
|
|
17
|
+
GET: { requireAuth: true, requireFeatures: ['billing.invoice.view'] },
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type InvoiceRow = {
|
|
21
|
+
id: string
|
|
22
|
+
invoice_number: string
|
|
23
|
+
status: string | null
|
|
24
|
+
status_entry_id: string | null
|
|
25
|
+
currency_code: string
|
|
26
|
+
issue_date: string | null
|
|
27
|
+
due_date: string | null
|
|
28
|
+
subtotal_net_amount: string
|
|
29
|
+
subtotal_gross_amount: string
|
|
30
|
+
discount_total_amount: string
|
|
31
|
+
tax_total_amount: string
|
|
32
|
+
grand_total_net_amount: string
|
|
33
|
+
grand_total_gross_amount: string
|
|
34
|
+
paid_total_amount: string
|
|
35
|
+
outstanding_amount: string
|
|
36
|
+
metadata: Record<string, unknown> | null
|
|
37
|
+
created_at: string
|
|
38
|
+
updated_at: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type InvoiceLineRow = {
|
|
42
|
+
id: string
|
|
43
|
+
line_number: number
|
|
44
|
+
kind: string
|
|
45
|
+
description: string | null
|
|
46
|
+
quantity: string
|
|
47
|
+
currency_code: string
|
|
48
|
+
unit_price_net: string
|
|
49
|
+
unit_price_gross: string
|
|
50
|
+
tax_rate: string
|
|
51
|
+
tax_amount: string
|
|
52
|
+
total_net_amount: string
|
|
53
|
+
total_gross_amount: string
|
|
54
|
+
metadata: Record<string, unknown> | null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function GET(
|
|
58
|
+
req: Request,
|
|
59
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
60
|
+
) {
|
|
61
|
+
try {
|
|
62
|
+
const container = await createRequestContainer()
|
|
63
|
+
const auth = await getAuthFromRequest(req)
|
|
64
|
+
if (!auth || !auth.tenantId) {
|
|
65
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
66
|
+
}
|
|
67
|
+
const organizationId = (auth as { orgId?: string }).orgId ?? null
|
|
68
|
+
if (!organizationId) {
|
|
69
|
+
return NextResponse.json(
|
|
70
|
+
{ error: 'Organization scope required' },
|
|
71
|
+
{ status: 403 },
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { id: invoiceId } = await params
|
|
76
|
+
const em = (container.resolve('em') as EntityManager).fork()
|
|
77
|
+
|
|
78
|
+
const invoiceRows = (await em.execute(
|
|
79
|
+
`SELECT id, invoice_number, status, status_entry_id, currency_code,
|
|
80
|
+
issue_date, due_date,
|
|
81
|
+
subtotal_net_amount, subtotal_gross_amount,
|
|
82
|
+
discount_total_amount, tax_total_amount,
|
|
83
|
+
grand_total_net_amount, grand_total_gross_amount,
|
|
84
|
+
paid_total_amount, outstanding_amount,
|
|
85
|
+
metadata, created_at, updated_at
|
|
86
|
+
FROM sales_invoices
|
|
87
|
+
WHERE id = ?
|
|
88
|
+
AND tenant_id = ?
|
|
89
|
+
AND organization_id = ?
|
|
90
|
+
AND deleted_at IS NULL
|
|
91
|
+
AND jsonb_exists(metadata, 'bill_run_id')`,
|
|
92
|
+
[invoiceId, auth.tenantId, organizationId],
|
|
93
|
+
)) as unknown as InvoiceRow[]
|
|
94
|
+
|
|
95
|
+
if (!invoiceRows || invoiceRows.length === 0) {
|
|
96
|
+
return NextResponse.json({ error: 'Invoice not found' }, { status: 404 })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const invoice = invoiceRows[0]
|
|
100
|
+
|
|
101
|
+
const lineRows = (await em.execute(
|
|
102
|
+
`SELECT id, line_number, kind, description, quantity, currency_code,
|
|
103
|
+
unit_price_net, unit_price_gross, tax_rate, tax_amount,
|
|
104
|
+
total_net_amount, total_gross_amount, metadata
|
|
105
|
+
FROM sales_invoice_lines
|
|
106
|
+
WHERE invoice_id = ?
|
|
107
|
+
AND tenant_id = ?
|
|
108
|
+
AND organization_id = ?
|
|
109
|
+
ORDER BY line_number ASC`,
|
|
110
|
+
[invoiceId, auth.tenantId, organizationId],
|
|
111
|
+
)) as unknown as InvoiceLineRow[]
|
|
112
|
+
|
|
113
|
+
return NextResponse.json({
|
|
114
|
+
invoice,
|
|
115
|
+
lines: lineRows,
|
|
116
|
+
})
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error('[billing/invoices/[id].GET]', err)
|
|
119
|
+
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const openApi = {
|
|
124
|
+
methods: {
|
|
125
|
+
GET: {
|
|
126
|
+
tags: ['Billing'],
|
|
127
|
+
summary: 'Get a billing-managed invoice (with lines)',
|
|
128
|
+
description:
|
|
129
|
+
'Returns the `core/sales` invoice + every line, scoped to ' +
|
|
130
|
+
"billing-managed invoices (`metadata.bill_run_id` set). Non-" +
|
|
131
|
+
'billing invoices return 404 even when the UUID resolves at ' +
|
|
132
|
+
'`/api/sales/invoices/[id]`.',
|
|
133
|
+
parameters: [
|
|
134
|
+
{
|
|
135
|
+
name: 'id',
|
|
136
|
+
in: 'path',
|
|
137
|
+
required: true,
|
|
138
|
+
schema: { type: 'string', format: 'uuid' },
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
3
|
+
import { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'
|
|
4
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
5
|
+
import { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'
|
|
6
|
+
import { billingEntityIds } from '../../../data/entityIds'
|
|
7
|
+
import { billingInvoiceAddLineSchema } from '../../../data/validators'
|
|
8
|
+
import { createBillingCrudOpenApi } from '../../openapi'
|
|
9
|
+
|
|
10
|
+
const routeMetadata = {
|
|
11
|
+
POST: { requireAuth: true, requireFeatures: ['billing.invoice.edit_draft'] },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const metadata = routeMetadata
|
|
15
|
+
|
|
16
|
+
const addLineResponseSchema = z.object({
|
|
17
|
+
invoiceId: z.string().uuid(),
|
|
18
|
+
invoiceLineId: z.string().uuid(),
|
|
19
|
+
auditId: z.string().uuid(),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const rawBodySchema = z.object({}).passthrough()
|
|
23
|
+
|
|
24
|
+
const crud = makeCrudRoute({
|
|
25
|
+
metadata: routeMetadata,
|
|
26
|
+
orm: {
|
|
27
|
+
entity: SalesInvoice,
|
|
28
|
+
idField: 'id',
|
|
29
|
+
orgField: 'organizationId',
|
|
30
|
+
tenantField: 'tenantId',
|
|
31
|
+
softDeleteField: 'deletedAt',
|
|
32
|
+
},
|
|
33
|
+
list: {
|
|
34
|
+
schema: z.object({}).passthrough(),
|
|
35
|
+
entityId: billingEntityIds.draftInvoiceEdit,
|
|
36
|
+
fields: ['id'],
|
|
37
|
+
buildFilters: async () => ({}),
|
|
38
|
+
},
|
|
39
|
+
actions: {
|
|
40
|
+
create: {
|
|
41
|
+
commandId: 'billing.invoices.add_draft_line',
|
|
42
|
+
schema: rawBodySchema,
|
|
43
|
+
mapInput: async ({ raw, ctx }) => {
|
|
44
|
+
const { translate } = await resolveTranslations()
|
|
45
|
+
const scoped = withScopedPayload(raw ?? {}, ctx, translate)
|
|
46
|
+
return billingInvoiceAddLineSchema.parse(scoped)
|
|
47
|
+
},
|
|
48
|
+
response: ({ result }) => ({
|
|
49
|
+
invoiceId: result?.invoiceId ?? null,
|
|
50
|
+
invoiceLineId: result?.invoiceLineId ?? null,
|
|
51
|
+
auditId: result?.auditId ?? null,
|
|
52
|
+
}),
|
|
53
|
+
status: 201,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export const POST = crud.POST
|
|
59
|
+
|
|
60
|
+
export const openApi = createBillingCrudOpenApi({
|
|
61
|
+
resourceName: 'BillingInvoiceLineAdd',
|
|
62
|
+
pluralName: 'BillingInvoiceLineAdds',
|
|
63
|
+
querySchema: z.object({}).passthrough(),
|
|
64
|
+
listResponseSchema: z.object({ items: z.array(z.unknown()) }),
|
|
65
|
+
create: {
|
|
66
|
+
schema: billingInvoiceAddLineSchema,
|
|
67
|
+
responseSchema: addLineResponseSchema,
|
|
68
|
+
description:
|
|
69
|
+
'Appends a new line to a draft invoice (e.g. operator adds a ' +
|
|
70
|
+
'one-off adjustment). Writes a `DraftInvoiceEdit` audit row ' +
|
|
71
|
+
'(action=`line_added`, `before_json=null`). Recomputes invoice ' +
|
|
72
|
+
'totals. Refuses non-draft invoices.',
|
|
73
|
+
},
|
|
74
|
+
})
|