@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,97 @@
|
|
|
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 {
|
|
8
|
+
billingInvoiceAddLineSchema,
|
|
9
|
+
billingInvoiceEditLineSchema,
|
|
10
|
+
billingInvoiceRemoveLineSchema,
|
|
11
|
+
} from '../../../data/validators'
|
|
12
|
+
import { createBillingCrudOpenApi } from '../../openapi'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `POST /api/billing/invoices/edit-line` — edits one line of a draft
|
|
16
|
+
* invoice. Refuses non-drafts (409 / `billing.invoice.not_draft`).
|
|
17
|
+
* Writes a `DraftInvoiceEdit` audit row per spec. Recomputes invoice
|
|
18
|
+
* totals from the surviving lines after the edit.
|
|
19
|
+
*
|
|
20
|
+
* Operator surface: route is gated by `billing.invoice.edit_draft`.
|
|
21
|
+
* Future Phase 4c admin UI calls into the same command via this route.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const routeMetadata = {
|
|
25
|
+
POST: { requireAuth: true, requireFeatures: ['billing.invoice.edit_draft'] },
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const metadata = routeMetadata
|
|
29
|
+
|
|
30
|
+
const editLineResponseSchema = z.object({
|
|
31
|
+
invoiceId: z.string().uuid(),
|
|
32
|
+
invoiceLineId: z.string().uuid(),
|
|
33
|
+
auditId: z.string().uuid(),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const rawBodySchema = z.object({}).passthrough()
|
|
37
|
+
|
|
38
|
+
const crud = makeCrudRoute({
|
|
39
|
+
metadata: routeMetadata,
|
|
40
|
+
orm: {
|
|
41
|
+
entity: SalesInvoice,
|
|
42
|
+
idField: 'id',
|
|
43
|
+
orgField: 'organizationId',
|
|
44
|
+
tenantField: 'tenantId',
|
|
45
|
+
softDeleteField: 'deletedAt',
|
|
46
|
+
},
|
|
47
|
+
list: {
|
|
48
|
+
schema: z.object({}).passthrough(),
|
|
49
|
+
entityId: billingEntityIds.draftInvoiceEdit,
|
|
50
|
+
fields: ['id'],
|
|
51
|
+
buildFilters: async () => ({}),
|
|
52
|
+
},
|
|
53
|
+
actions: {
|
|
54
|
+
create: {
|
|
55
|
+
commandId: 'billing.invoices.edit_draft_line',
|
|
56
|
+
schema: rawBodySchema,
|
|
57
|
+
mapInput: async ({ raw, ctx }) => {
|
|
58
|
+
const { translate } = await resolveTranslations()
|
|
59
|
+
const scoped = withScopedPayload(raw ?? {}, ctx, translate)
|
|
60
|
+
return billingInvoiceEditLineSchema.parse(scoped)
|
|
61
|
+
},
|
|
62
|
+
response: ({ result }) => ({
|
|
63
|
+
invoiceId: result?.invoiceId ?? null,
|
|
64
|
+
invoiceLineId: result?.invoiceLineId ?? null,
|
|
65
|
+
auditId: result?.auditId ?? null,
|
|
66
|
+
}),
|
|
67
|
+
status: 200,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
export const POST = crud.POST
|
|
73
|
+
|
|
74
|
+
export const openApi = createBillingCrudOpenApi({
|
|
75
|
+
resourceName: 'BillingInvoiceLineEdit',
|
|
76
|
+
pluralName: 'BillingInvoiceLineEdits',
|
|
77
|
+
querySchema: z.object({}).passthrough(),
|
|
78
|
+
listResponseSchema: z.object({ items: z.array(z.unknown()) }),
|
|
79
|
+
create: {
|
|
80
|
+
schema: billingInvoiceEditLineSchema,
|
|
81
|
+
responseSchema: editLineResponseSchema,
|
|
82
|
+
description:
|
|
83
|
+
'Edits one line of a draft invoice (description, quantity, ' +
|
|
84
|
+
"unit_price, or explicit `totalNetAmount` override). Writes a " +
|
|
85
|
+
'`DraftInvoiceEdit` audit row with before+after snapshots and ' +
|
|
86
|
+
'recomputes invoice totals. Refuses non-draft invoices with ' +
|
|
87
|
+
'HTTP 409 + `billing.invoice.not_draft`.',
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Re-export the add/remove route schemas so the OpenAPI bundle keeps
|
|
92
|
+
// them discoverable from a single import surface — the dedicated
|
|
93
|
+
// routes (sibling folders) own the actual handlers.
|
|
94
|
+
export const _phaseSchemaRefs = {
|
|
95
|
+
add: billingInvoiceAddLineSchema,
|
|
96
|
+
remove: billingInvoiceRemoveLineSchema,
|
|
97
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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 { billingInvoicePostSchema } from '../../../data/validators'
|
|
8
|
+
import { createBillingCrudOpenApi } from '../../openapi'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `POST /api/billing/invoices/post`
|
|
12
|
+
*
|
|
13
|
+
* Operator action: flip a billing-drafted invoice from `draft` →
|
|
14
|
+
* `posted`. The command refuses non-draft inputs (409 with
|
|
15
|
+
* `billing.invoice.not_draft`) and test-mode invoices (409 with
|
|
16
|
+
* `billing.invoice.test_run`). The invoice number is NOT re-assigned
|
|
17
|
+
* (it was set at draft create — see Phase 2 deviation note in the
|
|
18
|
+
* spec).
|
|
19
|
+
*
|
|
20
|
+
* Implemented as a create-only `makeCrudRoute` so it inherits the same
|
|
21
|
+
* RBAC + OpenAPI plumbing as the rest of the billing surface. The
|
|
22
|
+
* `entity` is `SalesInvoice` only because the factory requires one;
|
|
23
|
+
* the GET half is never exposed on this URL.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const routeMetadata = {
|
|
27
|
+
POST: { requireAuth: true, requireFeatures: ['billing.invoice.post'] },
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const metadata = routeMetadata
|
|
31
|
+
|
|
32
|
+
const postResponseSchema = z.object({
|
|
33
|
+
invoiceId: z.string().uuid(),
|
|
34
|
+
invoiceNumber: z.string(),
|
|
35
|
+
status: z.literal('posted'),
|
|
36
|
+
lineCount: z.number().int().nonnegative(),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const rawBodySchema = z.object({}).passthrough()
|
|
40
|
+
|
|
41
|
+
const crud = makeCrudRoute({
|
|
42
|
+
metadata: routeMetadata,
|
|
43
|
+
orm: {
|
|
44
|
+
entity: SalesInvoice,
|
|
45
|
+
idField: 'id',
|
|
46
|
+
orgField: 'organizationId',
|
|
47
|
+
tenantField: 'tenantId',
|
|
48
|
+
softDeleteField: 'deletedAt',
|
|
49
|
+
},
|
|
50
|
+
list: {
|
|
51
|
+
schema: z.object({}).passthrough(),
|
|
52
|
+
// `entityId` is technically a core/sales concept, but the factory
|
|
53
|
+
// only needs it as an identifier — using a billing-namespaced one
|
|
54
|
+
// keeps the registry tidy (no surprise list endpoints).
|
|
55
|
+
entityId: billingEntityIds.draftInvoiceEdit,
|
|
56
|
+
fields: ['id'],
|
|
57
|
+
buildFilters: async () => ({}),
|
|
58
|
+
},
|
|
59
|
+
actions: {
|
|
60
|
+
create: {
|
|
61
|
+
commandId: 'billing.invoices.post',
|
|
62
|
+
schema: rawBodySchema,
|
|
63
|
+
mapInput: async ({ raw, ctx }) => {
|
|
64
|
+
const { translate } = await resolveTranslations()
|
|
65
|
+
const scoped = withScopedPayload(raw ?? {}, ctx, translate)
|
|
66
|
+
return billingInvoicePostSchema.parse(scoped)
|
|
67
|
+
},
|
|
68
|
+
response: ({ result }) => ({
|
|
69
|
+
invoiceId: result?.invoiceId ?? null,
|
|
70
|
+
invoiceNumber: result?.invoiceNumber ?? null,
|
|
71
|
+
status: result?.status ?? null,
|
|
72
|
+
lineCount: result?.lineCount ?? 0,
|
|
73
|
+
}),
|
|
74
|
+
status: 200,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
export const POST = crud.POST
|
|
80
|
+
|
|
81
|
+
export const openApi = createBillingCrudOpenApi({
|
|
82
|
+
resourceName: 'BillingInvoicePost',
|
|
83
|
+
pluralName: 'BillingInvoicePosts',
|
|
84
|
+
querySchema: z.object({}).passthrough(),
|
|
85
|
+
listResponseSchema: z.object({ items: z.array(z.unknown()) }),
|
|
86
|
+
create: {
|
|
87
|
+
schema: billingInvoicePostSchema,
|
|
88
|
+
responseSchema: postResponseSchema,
|
|
89
|
+
description:
|
|
90
|
+
'Flips a billing-drafted `core/sales` invoice from `draft` to ' +
|
|
91
|
+
'`posted`. Refuses non-draft invoices (409 / ' +
|
|
92
|
+
'`billing.invoice.not_draft`) and test-mode invoices (409 / ' +
|
|
93
|
+
'`billing.invoice.test_run`). Fires `billing.invoice.posted` + ' +
|
|
94
|
+
'one `billing.invoice.line_posted` per line. The invoice number ' +
|
|
95
|
+
'is NOT re-assigned (set at draft create — see Phase 2 deviation).',
|
|
96
|
+
},
|
|
97
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
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 { billingInvoiceRemoveLineSchema } 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 removeLineResponseSchema = 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.remove_draft_line',
|
|
42
|
+
schema: rawBodySchema,
|
|
43
|
+
mapInput: async ({ raw, ctx }) => {
|
|
44
|
+
const { translate } = await resolveTranslations()
|
|
45
|
+
const scoped = withScopedPayload(raw ?? {}, ctx, translate)
|
|
46
|
+
return billingInvoiceRemoveLineSchema.parse(scoped)
|
|
47
|
+
},
|
|
48
|
+
response: ({ result }) => ({
|
|
49
|
+
invoiceId: result?.invoiceId ?? null,
|
|
50
|
+
invoiceLineId: result?.invoiceLineId ?? null,
|
|
51
|
+
auditId: result?.auditId ?? null,
|
|
52
|
+
}),
|
|
53
|
+
status: 200,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export const POST = crud.POST
|
|
59
|
+
|
|
60
|
+
export const openApi = createBillingCrudOpenApi({
|
|
61
|
+
resourceName: 'BillingInvoiceLineRemove',
|
|
62
|
+
pluralName: 'BillingInvoiceLineRemoves',
|
|
63
|
+
querySchema: z.object({}).passthrough(),
|
|
64
|
+
listResponseSchema: z.object({ items: z.array(z.unknown()) }),
|
|
65
|
+
create: {
|
|
66
|
+
schema: billingInvoiceRemoveLineSchema,
|
|
67
|
+
responseSchema: removeLineResponseSchema,
|
|
68
|
+
description:
|
|
69
|
+
'Removes one line from a draft invoice (hard delete — the line ' +
|
|
70
|
+
'never existed as far as future readers are concerned). Writes ' +
|
|
71
|
+
'a `DraftInvoiceEdit` audit row (action=`line_removed`, ' +
|
|
72
|
+
'`after_json=null`) carrying the before-snapshot for forensics. ' +
|
|
73
|
+
'Recomputes invoice totals. Refuses non-draft invoices.',
|
|
74
|
+
},
|
|
75
|
+
})
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `GET /api/billing/invoices`
|
|
9
|
+
*
|
|
10
|
+
* Lists `core/sales` invoices that the billing engine emitted (i.e.
|
|
11
|
+
* carry `metadata.bill_run_id`). Hand-rolled rather than wrapped via
|
|
12
|
+
* `makeCrudRoute` because the filter predicate is a JSONB
|
|
13
|
+
* `metadata->>'bill_run_id' IS NOT NULL` check that doesn't map
|
|
14
|
+
* cleanly onto the factory's `where`-shaped filter API, and the
|
|
15
|
+
* entity itself is owned by `core/sales` — keeping the surface
|
|
16
|
+
* read-only and hand-rolled avoids surprising the cross-module
|
|
17
|
+
* reviewer.
|
|
18
|
+
*
|
|
19
|
+
* Filters:
|
|
20
|
+
* - `status` — draft / posted / paid / void
|
|
21
|
+
* - `billRunId` — exact match on `metadata->>'bill_run_id'`
|
|
22
|
+
* - `billAccountId` — exact match on `metadata->>'bill_account_id'`
|
|
23
|
+
* - `testRun=true|false` — flips the `metadata->>'test_run'` filter
|
|
24
|
+
* - `search` — substring on invoice_number
|
|
25
|
+
*
|
|
26
|
+
* Sort: started_at DESC (most recent first). Pagination: standard
|
|
27
|
+
* page / pageSize (capped at 100).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export const metadata = {
|
|
31
|
+
GET: { requireAuth: true, requireFeatures: ['billing.invoice.view'] },
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const querySchema = z
|
|
35
|
+
.object({
|
|
36
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
37
|
+
pageSize: z.coerce.number().int().min(1).max(100).default(50),
|
|
38
|
+
status: z.enum(['draft', 'posted', 'paid', 'void']).optional(),
|
|
39
|
+
billRunId: z.string().uuid().optional(),
|
|
40
|
+
billAccountId: z.string().uuid().optional(),
|
|
41
|
+
testRun: z.coerce.boolean().optional(),
|
|
42
|
+
search: z.string().optional(),
|
|
43
|
+
})
|
|
44
|
+
.passthrough()
|
|
45
|
+
|
|
46
|
+
type BillingInvoiceRow = {
|
|
47
|
+
id: string
|
|
48
|
+
invoice_number: string
|
|
49
|
+
status: string | null
|
|
50
|
+
currency_code: string
|
|
51
|
+
issue_date: string | null
|
|
52
|
+
due_date: string | null
|
|
53
|
+
subtotal_net_amount: string
|
|
54
|
+
grand_total_net_amount: string
|
|
55
|
+
grand_total_gross_amount: string
|
|
56
|
+
paid_total_amount: string
|
|
57
|
+
outstanding_amount: string
|
|
58
|
+
metadata: Record<string, unknown> | null
|
|
59
|
+
created_at: string
|
|
60
|
+
updated_at: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function GET(req: Request) {
|
|
64
|
+
try {
|
|
65
|
+
const container = await createRequestContainer()
|
|
66
|
+
const auth = await getAuthFromRequest(req)
|
|
67
|
+
if (!auth || !auth.tenantId) {
|
|
68
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
69
|
+
}
|
|
70
|
+
const organizationId = (auth as { orgId?: string }).orgId ?? null
|
|
71
|
+
if (!organizationId) {
|
|
72
|
+
return NextResponse.json(
|
|
73
|
+
{ error: 'Organization scope required' },
|
|
74
|
+
{ status: 403 },
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const url = new URL(req.url)
|
|
79
|
+
const query = querySchema.parse(
|
|
80
|
+
Object.fromEntries(url.searchParams.entries()),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const em = (container.resolve('em') as EntityManager).fork()
|
|
84
|
+
|
|
85
|
+
// Build the predicate. `jsonb_exists(metadata, 'bill_run_id')` is
|
|
86
|
+
// the function form of the JSONB `?` "key exists" operator — the
|
|
87
|
+
// operator form cannot be used here because `em.execute` treats a
|
|
88
|
+
// literal `?` as a positional parameter placeholder. Keeps us
|
|
89
|
+
// narrow to billing-managed invoices, never bleeding into invoices
|
|
90
|
+
// written by other modules.
|
|
91
|
+
const whereClauses: string[] = [
|
|
92
|
+
'tenant_id = ?',
|
|
93
|
+
'organization_id = ?',
|
|
94
|
+
'deleted_at IS NULL',
|
|
95
|
+
"jsonb_exists(metadata, 'bill_run_id')",
|
|
96
|
+
]
|
|
97
|
+
const params: unknown[] = [auth.tenantId, organizationId]
|
|
98
|
+
|
|
99
|
+
if (query.status) {
|
|
100
|
+
whereClauses.push('status = ?')
|
|
101
|
+
params.push(query.status)
|
|
102
|
+
}
|
|
103
|
+
if (query.billRunId) {
|
|
104
|
+
whereClauses.push("metadata->>'bill_run_id' = ?")
|
|
105
|
+
params.push(query.billRunId)
|
|
106
|
+
}
|
|
107
|
+
if (query.billAccountId) {
|
|
108
|
+
whereClauses.push("metadata->>'bill_account_id' = ?")
|
|
109
|
+
params.push(query.billAccountId)
|
|
110
|
+
}
|
|
111
|
+
if (typeof query.testRun === 'boolean') {
|
|
112
|
+
if (query.testRun) {
|
|
113
|
+
whereClauses.push("metadata->>'test_run' = 'true'")
|
|
114
|
+
} else {
|
|
115
|
+
whereClauses.push("(metadata->>'test_run' IS NULL OR metadata->>'test_run' != 'true')")
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (typeof query.search === 'string' && query.search.trim()) {
|
|
119
|
+
whereClauses.push('invoice_number ILIKE ?')
|
|
120
|
+
params.push(`%${query.search.trim().replace(/[%_]/g, (m) => `\\${m}`)}%`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const whereSql = whereClauses.join(' AND ')
|
|
124
|
+
const countRows = (await em.execute(
|
|
125
|
+
`SELECT COUNT(*)::int AS total FROM sales_invoices WHERE ${whereSql}`,
|
|
126
|
+
params,
|
|
127
|
+
)) as unknown as Array<{ total: number }>
|
|
128
|
+
const total = countRows?.[0]?.total ?? 0
|
|
129
|
+
|
|
130
|
+
const offset = (query.page - 1) * query.pageSize
|
|
131
|
+
const itemRows = (await em.execute(
|
|
132
|
+
`SELECT id, invoice_number, status, currency_code, issue_date, due_date,
|
|
133
|
+
subtotal_net_amount, grand_total_net_amount, grand_total_gross_amount,
|
|
134
|
+
paid_total_amount, outstanding_amount, metadata, created_at, updated_at
|
|
135
|
+
FROM sales_invoices
|
|
136
|
+
WHERE ${whereSql}
|
|
137
|
+
ORDER BY created_at DESC
|
|
138
|
+
LIMIT ? OFFSET ?`,
|
|
139
|
+
[...params, query.pageSize, offset],
|
|
140
|
+
)) as unknown as BillingInvoiceRow[]
|
|
141
|
+
|
|
142
|
+
return NextResponse.json({
|
|
143
|
+
items: itemRows,
|
|
144
|
+
total,
|
|
145
|
+
page: query.page,
|
|
146
|
+
pageSize: query.pageSize,
|
|
147
|
+
totalPages: total === 0 ? 1 : Math.ceil(total / query.pageSize),
|
|
148
|
+
})
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error('[billing/invoices.GET]', err)
|
|
151
|
+
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const openApi = {
|
|
156
|
+
methods: {
|
|
157
|
+
GET: {
|
|
158
|
+
tags: ['Billing'],
|
|
159
|
+
summary: 'List billing-managed invoices',
|
|
160
|
+
description:
|
|
161
|
+
'Returns a paginated list of `core/sales` invoices that the ' +
|
|
162
|
+
'billing engine emitted (matched via `metadata->>\'bill_run_id\'` ' +
|
|
163
|
+
'presence). Supports status / billRunId / billAccountId / testRun ' +
|
|
164
|
+
'/ invoice_number search filters.',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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 { BillingItem } from '../../../data/entities'
|
|
6
|
+
import { billingEntityIds } from '../../../data/entityIds'
|
|
7
|
+
import { billingItemBulkCreateSchema } from '../../../data/validators'
|
|
8
|
+
import { createBillingCrudOpenApi } from '../../openapi'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `POST /api/billing/items/bulk` — batch-create Billing Items.
|
|
12
|
+
*
|
|
13
|
+
* One scope envelope, up to 500 item payloads. Idempotent per
|
|
14
|
+
* `source_ref` with a *single* batched existence check, then a
|
|
15
|
+
* single flush. The CPQ connector's subscription-activated /
|
|
16
|
+
* -amended / -renewed subscribers are the primary callers — bulk
|
|
17
|
+
* collapses what used to be one `billing.items.create` round-trip
|
|
18
|
+
* per charge.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const routeMetadata = {
|
|
22
|
+
POST: { requireAuth: true, requireFeatures: ['billing.item.manage'] },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const metadata = routeMetadata
|
|
26
|
+
|
|
27
|
+
const bulkResultEntrySchema = z.object({
|
|
28
|
+
sourceRef: z.string().nullable(),
|
|
29
|
+
id: z.string().uuid(),
|
|
30
|
+
deduplicated: z.boolean(),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const bulkResponseSchema = z.object({
|
|
34
|
+
created: z.number().int().nonnegative(),
|
|
35
|
+
deduplicated: z.number().int().nonnegative(),
|
|
36
|
+
items: z.array(bulkResultEntrySchema),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const rawBodySchema = z.object({}).passthrough()
|
|
40
|
+
|
|
41
|
+
const crud = makeCrudRoute({
|
|
42
|
+
metadata: routeMetadata,
|
|
43
|
+
orm: {
|
|
44
|
+
entity: BillingItem,
|
|
45
|
+
idField: 'id',
|
|
46
|
+
orgField: 'organizationId',
|
|
47
|
+
tenantField: 'tenantId',
|
|
48
|
+
softDeleteField: 'deletedAt',
|
|
49
|
+
},
|
|
50
|
+
list: {
|
|
51
|
+
schema: z.object({}).passthrough(),
|
|
52
|
+
entityId: billingEntityIds.item,
|
|
53
|
+
fields: ['id'],
|
|
54
|
+
buildFilters: async () => ({}),
|
|
55
|
+
},
|
|
56
|
+
actions: {
|
|
57
|
+
create: {
|
|
58
|
+
commandId: 'billing.items.bulk_create',
|
|
59
|
+
schema: rawBodySchema,
|
|
60
|
+
mapInput: async ({ raw, ctx }) => {
|
|
61
|
+
const { translate } = await resolveTranslations()
|
|
62
|
+
const scoped = withScopedPayload(raw ?? {}, ctx, translate)
|
|
63
|
+
return billingItemBulkCreateSchema.parse(scoped)
|
|
64
|
+
},
|
|
65
|
+
response: ({ result }) => ({
|
|
66
|
+
created: result?.created ?? 0,
|
|
67
|
+
deduplicated: result?.deduplicated ?? 0,
|
|
68
|
+
items: result?.items ?? [],
|
|
69
|
+
}),
|
|
70
|
+
status: 200,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export const POST = crud.POST
|
|
76
|
+
|
|
77
|
+
export const openApi = createBillingCrudOpenApi({
|
|
78
|
+
resourceName: 'BillingItemBulkCreate',
|
|
79
|
+
pluralName: 'BillingItemBulkCreates',
|
|
80
|
+
querySchema: z.object({}).passthrough(),
|
|
81
|
+
listResponseSchema: z.object({ items: z.array(z.unknown()) }),
|
|
82
|
+
create: {
|
|
83
|
+
schema: billingItemBulkCreateSchema,
|
|
84
|
+
responseSchema: bulkResponseSchema,
|
|
85
|
+
description:
|
|
86
|
+
'Batch-creates up to 500 Billing Items under one scope envelope. ' +
|
|
87
|
+
'Idempotent per `sourceRef` (one batched existence check). ' +
|
|
88
|
+
'Returns `{ created, deduplicated, items: [{ sourceRef, id, ' +
|
|
89
|
+
'deduplicated }] }`. Always HTTP 200 — duplicates are reported ' +
|
|
90
|
+
'in the body, never an error.',
|
|
91
|
+
},
|
|
92
|
+
})
|