@dainamite/billing 0.21.0 → 0.22.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 +13 -0
- package/dist/modules/billing/api/accounts/route.js +2 -1
- package/dist/modules/billing/api/accounts/route.js.map +2 -2
- package/dist/modules/billing/api/invoices/[id]/pdf/route.js +2 -1
- package/dist/modules/billing/api/invoices/[id]/pdf/route.js.map +2 -2
- package/dist/modules/billing/data/entities.js.map +2 -2
- package/dist/modules/billing/i18n/de.json +880 -0
- package/dist/modules/billing/i18n/es.json +880 -0
- package/dist/modules/billing/i18n/pl.json +1 -1
- package/dist/modules/billing/lib/invoiceDelivery.js +4 -2
- package/dist/modules/billing/lib/invoiceDelivery.js.map +2 -2
- package/dist/modules/billing/lib/invoiceLanguages.js +15 -3
- package/dist/modules/billing/lib/invoiceLanguages.js.map +2 -2
- package/dist/modules/billing/lib/pdf/invoiceModel.js +2 -2
- package/dist/modules/billing/lib/pdf/invoiceModel.js.map +2 -2
- package/dist/modules/billing/lib/pdf/invoicePdf.js +3 -3
- package/dist/modules/billing/lib/pdf/invoicePdf.js.map +2 -2
- package/dist/modules/billing/lib/pdf/invoiceTemplate.js +3 -1
- package/dist/modules/billing/lib/pdf/invoiceTemplate.js.map +2 -2
- package/package.json +1 -1
- package/src/modules/billing/__integration__/TC-BILL-004.spec.ts +2 -1
- package/src/modules/billing/__tests__/i18n/dictionaries.test.ts +64 -0
- package/src/modules/billing/__tests__/lib/invoiceDelivery.test.ts +17 -0
- package/src/modules/billing/__tests__/lib/invoiceLanguages.test.ts +49 -0
- package/src/modules/billing/__tests__/lib/pdf/invoicePdf.test.ts +75 -5
- package/src/modules/billing/__tests__/validators.test.ts +5 -1
- package/src/modules/billing/api/accounts/route.ts +2 -1
- package/src/modules/billing/api/invoices/[id]/pdf/route.ts +2 -1
- package/src/modules/billing/data/entities.ts +2 -1
- package/src/modules/billing/i18n/de.json +880 -0
- package/src/modules/billing/i18n/es.json +880 -0
- package/src/modules/billing/i18n/pl.json +1 -1
- package/src/modules/billing/lib/invoiceDelivery.ts +4 -5
- package/src/modules/billing/lib/invoiceLanguages.ts +34 -11
- package/src/modules/billing/lib/pdf/invoiceModel.ts +5 -4
- package/src/modules/billing/lib/pdf/invoicePdf.ts +8 -4
- package/src/modules/billing/lib/pdf/invoiceTemplate.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @dainamite/billing
|
|
2
2
|
|
|
3
|
+
## 0.22.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 34d3ac4: Spanish and German for the billing UI and billing documents.
|
|
8
|
+
|
|
9
|
+
- **Billing screens in Spanish and German.** New `i18n/es.json` and `i18n/de.json` (all 878 keys). Operators whose UI locale is `es` or `de` now see billing in their language, consistent with the core modules.
|
|
10
|
+
- **Invoice languages `es` and `de`.** `invoiceLanguage` on `POST`/`PUT /api/billing/accounts` now accepts `en | es | de | pl` (additive). Invoices, credit and debit memos, receipts and the invoice email render in the account's language, with `es-ES` / `de-DE` money and date formats. One neutral wording per language (e.g. ES _Factura rectificativa_, _IVA_, _NIF_; DE _Rechnungskorrektur_, _USt._, _USt-IdNr._). The payment transfer title still carries the invoice number.
|
|
11
|
+
- **Fix:** adding a language no longer falls back to English silently. `lib/invoiceLanguages.ts` is the single source for the picklist, the validator, the `Intl` locale and the document translator, and a test fails if a supported language has no dictionary or a translation drops a `{placeholder}`.
|
|
12
|
+
- **Fix (pl):** the weekly cycle-anchor placeholder showed `pon | wt | … | nd`, which the validator rejects. It now shows the accepted values `mon | tue | … | sun`.
|
|
13
|
+
|
|
14
|
+
No migration. After upgrading run `yarn generate` so the app picks up the new dictionaries.
|
|
15
|
+
|
|
3
16
|
## 0.21.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
|
@@ -6,6 +6,7 @@ import { BillingAccount } from "../../data/entities.js";
|
|
|
6
6
|
import { fetchAccountFinancials, fetchOverdueAccountIds, financialsOrZero } from "../../lib/accountFinancials.js";
|
|
7
7
|
const NO_MATCH_ID = "00000000-0000-4000-8000-000000000000";
|
|
8
8
|
import { billingEntityIds } from "../../data/entityIds.js";
|
|
9
|
+
import { DEFAULT_INVOICE_LANGUAGE, INVOICE_LANGUAGE_CODES } from "../../lib/invoiceLanguages.js";
|
|
9
10
|
import {
|
|
10
11
|
billingAccountCreateSchema,
|
|
11
12
|
billingAccountListQuerySchema,
|
|
@@ -207,7 +208,7 @@ const openApi = createBillingCrudOpenApi({
|
|
|
207
208
|
create: {
|
|
208
209
|
schema: billingAccountCreateSchema,
|
|
209
210
|
responseSchema: defaultCreateResponseSchema,
|
|
210
|
-
description:
|
|
211
|
+
description: `Creates a new billing account for a customer \u2014 a company or a person (\`customerKind\`, default \`company\`). \`invoiceLanguage\` must be a language supported by the invoice template (${INVOICE_LANGUAGE_CODES.map((c) => `\`${c}\``).join(", ")}; default \`${DEFAULT_INVOICE_LANGUAGE}\`).`
|
|
211
212
|
},
|
|
212
213
|
update: {
|
|
213
214
|
schema: billingAccountUpdateSchema,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/billing/api/accounts/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { BillingAccount } from '../../data/entities'\nimport { fetchAccountFinancials, fetchOverdueAccountIds, financialsOrZero } from '../../lib/accountFinancials'\n\n/** A uuid no row has \u2014 narrows an `$in` filter to nothing. */\nconst NO_MATCH_ID = '00000000-0000-4000-8000-000000000000'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n billingAccountCreateSchema,\n billingAccountListQuerySchema,\n billingAccountUpdateSchema,\n} from '../../data/validators'\nimport {\n createBillingCrudOpenApi,\n createPagedListResponseSchema,\n defaultCreateResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\n// \u2500\u2500\u2500 Auth / RBAC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.account.view'] },\n POST: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n}\n\nexport const metadata = routeMetadata\n\n// \u2500\u2500\u2500 List response item shape (informational \u2014 used by OpenAPI) \u2500\u2500\n\n// The list endpoint projects raw column names \u2014 snake_case.\nconst billingAccountListItemSchema = z.object({\n id: z.string().uuid(),\n customer_id: z.string(),\n customer_kind: z.enum(['company', 'person']),\n name: z.string(),\n currency_code: z.string(),\n bill_cycle: z.string(),\n bill_cycle_anchor: z.string(),\n invoice_email: z.string(),\n invoice_language: z.string(),\n tax_id: z.string().nullable(),\n invoice_address: z.record(z.string(), z.unknown()),\n next_bill_date: z.string(),\n last_bill_date: z.string().nullable(),\n billing_mode: z.string(),\n credit_limit: z.string(),\n payment_terms_days: z.number().int().nullable(),\n auto_send_invoice: z.boolean(),\n /** Spec F (F8): \u03A3 outstanding of open receivables and its overdue subset (batched per page). */\n balance_due: z.string(),\n overdue_amount: z.string(),\n is_active: z.boolean(),\n organization_id: z.string().uuid(),\n tenant_id: z.string().uuid(),\n created_at: z.string(),\n updated_at: z.string(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\n// \u2500\u2500\u2500 CRUD route \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingAccount,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: billingAccountListQuerySchema,\n entityId: billingEntityIds.account,\n fields: [\n 'id',\n 'customer_id',\n 'customer_kind',\n 'name',\n 'currency_code',\n 'bill_cycle',\n 'bill_cycle_anchor',\n 'invoice_email',\n 'invoice_language',\n 'tax_id',\n 'invoice_address',\n 'next_bill_date',\n 'last_bill_date',\n 'billing_mode',\n 'credit_limit',\n 'payment_terms_days',\n 'auto_send_invoice',\n 'is_active',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n name: 'name',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n nextBillDate: 'next_bill_date',\n },\n buildFilters: async (query, ctx) => {\n const filters: Record<string, unknown> = {}\n // Spec F (F8): accounts with an overdue receivable \u2014 one indexed\n // DISTINCT over billing_invoices, intersected with any `id` filter.\n if (query.hasOverdue === true && ctx.auth?.tenantId) {\n const organizationIds = ctx.organizationIds ?? (ctx.selectedOrganizationId ? [ctx.selectedOrganizationId] : [])\n const em = ctx.container.resolve<EntityManager>('em')\n const overdueIds = await fetchOverdueAccountIds(em, { tenantId: ctx.auth.tenantId, organizationIds })\n filters.id = { $in: overdueIds.length > 0 ? overdueIds : [NO_MATCH_ID] }\n }\n // `?id=<uuid>` narrows the list to one row \u2014 the detail page\n // reads a single account through this filter.\n if (typeof query.id === 'string' && query.id) {\n const current = filters.id as { $in?: string[] } | undefined\n filters.id = current?.$in ? { $in: current.$in.includes(query.id) ? [query.id] : [NO_MATCH_ID] } : { $eq: query.id }\n }\n if (typeof query.customerId === 'string' && query.customerId) {\n filters.customer_id = { $eq: query.customerId }\n }\n if (typeof query.customerKind === 'string' && query.customerKind) {\n filters.customer_kind = { $eq: query.customerKind }\n }\n if (typeof query.currencyCode === 'string' && query.currencyCode) {\n filters.currency_code = { $eq: query.currencyCode.toUpperCase() }\n }\n if (typeof query.billCycle === 'string' && query.billCycle) {\n filters.bill_cycle = { $eq: query.billCycle }\n }\n if (typeof query.billingMode === 'string' && query.billingMode) {\n filters.billing_mode = { $eq: query.billingMode }\n }\n if (typeof query.isActive === 'boolean') {\n filters.is_active = { $eq: query.isActive }\n }\n if (typeof query.search === 'string' && query.search.trim()) {\n const pattern = `%${query.search.trim().replace(/[%_]/g, (m) => `\\\\${m}`)}%`\n filters.name = { $ilike: pattern }\n }\n return filters\n },\n },\n hooks: {\n // Spec F (F8): balance due / overdue per row \u2014 one GROUP BY for the page.\n afterList: async (payload: unknown, ctx: unknown) => {\n const items = ((payload as { items?: unknown[] } | null)?.items ?? []) as Array<Record<string, unknown>>\n if (items.length === 0) return\n const first = items[0]\n const tenantId = typeof first.tenant_id === 'string' ? first.tenant_id : null\n const organizationId = typeof first.organization_id === 'string' ? first.organization_id : null\n const ids = items.map((row) => row.id).filter((id): id is string => typeof id === 'string')\n const financials =\n tenantId && organizationId\n ? await fetchAccountFinancials(\n (ctx as { container: AwilixContainer }).container.resolve<EntityManager>('em'),\n { tenantId, organizationId },\n ids,\n )\n : new Map()\n for (const row of items) {\n const values = financialsOrZero(financials, String(row.id))\n row.balance_due = values.balance_due\n row.overdue_amount = values.overdue_amount\n }\n },\n },\n actions: {\n create: {\n commandId: 'billing.accounts.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingAccountCreateSchema.parse(scoped)\n },\n response: ({ result }) => ({ id: result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'billing.accounts.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingAccountUpdateSchema.parse(scoped)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'billing.accounts.delete',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n // The factory hands delete a `{ body, query }` envelope \u2014 the\n // id may arrive in either, depending on the caller.\n const envelope = (raw ?? {}) as {\n body?: Record<string, unknown>\n query?: Record<string, unknown>\n }\n const body = envelope.body ?? {}\n const query = envelope.query ?? {}\n const scoped = withScopedPayload(body, ctx, translate)\n const id =\n (typeof body.id === 'string' && body.id) ||\n (typeof query.id === 'string' && query.id) ||\n null\n if (!id) {\n throw new Error('id is required')\n }\n return {\n id,\n tenantId: (scoped as { tenantId?: string }).tenantId,\n organizationId: (scoped as { organizationId?: string }).organizationId,\n }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingAccount',\n pluralName: 'BillingAccounts',\n querySchema: billingAccountListQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billingAccountListItemSchema),\n create: {\n schema: billingAccountCreateSchema,\n responseSchema: defaultCreateResponseSchema,\n description:\n 'Creates a new billing account for a customer \u2014 a company or a person (`customerKind`, default `company`). ' +\n '`invoiceLanguage` must be a language supported by the invoice template (`en`, `pl`; default `en`).',\n },\n update: {\n schema: billingAccountUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description:\n 'Updates a billing account. `currencyCode`, `customerId` and `customerKind` are immutable.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a billing account (sets `deleted_at`).',\n },\n})\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { BillingAccount } from '../../data/entities'\nimport { fetchAccountFinancials, fetchOverdueAccountIds, financialsOrZero } from '../../lib/accountFinancials'\n\n/** A uuid no row has \u2014 narrows an `$in` filter to nothing. */\nconst NO_MATCH_ID = '00000000-0000-4000-8000-000000000000'\nimport { billingEntityIds } from '../../data/entityIds'\nimport { DEFAULT_INVOICE_LANGUAGE, INVOICE_LANGUAGE_CODES } from '../../lib/invoiceLanguages'\nimport {\n billingAccountCreateSchema,\n billingAccountListQuerySchema,\n billingAccountUpdateSchema,\n} from '../../data/validators'\nimport {\n createBillingCrudOpenApi,\n createPagedListResponseSchema,\n defaultCreateResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\n// \u2500\u2500\u2500 Auth / RBAC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.account.view'] },\n POST: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n}\n\nexport const metadata = routeMetadata\n\n// \u2500\u2500\u2500 List response item shape (informational \u2014 used by OpenAPI) \u2500\u2500\n\n// The list endpoint projects raw column names \u2014 snake_case.\nconst billingAccountListItemSchema = z.object({\n id: z.string().uuid(),\n customer_id: z.string(),\n customer_kind: z.enum(['company', 'person']),\n name: z.string(),\n currency_code: z.string(),\n bill_cycle: z.string(),\n bill_cycle_anchor: z.string(),\n invoice_email: z.string(),\n invoice_language: z.string(),\n tax_id: z.string().nullable(),\n invoice_address: z.record(z.string(), z.unknown()),\n next_bill_date: z.string(),\n last_bill_date: z.string().nullable(),\n billing_mode: z.string(),\n credit_limit: z.string(),\n payment_terms_days: z.number().int().nullable(),\n auto_send_invoice: z.boolean(),\n /** Spec F (F8): \u03A3 outstanding of open receivables and its overdue subset (batched per page). */\n balance_due: z.string(),\n overdue_amount: z.string(),\n is_active: z.boolean(),\n organization_id: z.string().uuid(),\n tenant_id: z.string().uuid(),\n created_at: z.string(),\n updated_at: z.string(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\n// \u2500\u2500\u2500 CRUD route \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingAccount,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: billingAccountListQuerySchema,\n entityId: billingEntityIds.account,\n fields: [\n 'id',\n 'customer_id',\n 'customer_kind',\n 'name',\n 'currency_code',\n 'bill_cycle',\n 'bill_cycle_anchor',\n 'invoice_email',\n 'invoice_language',\n 'tax_id',\n 'invoice_address',\n 'next_bill_date',\n 'last_bill_date',\n 'billing_mode',\n 'credit_limit',\n 'payment_terms_days',\n 'auto_send_invoice',\n 'is_active',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n name: 'name',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n nextBillDate: 'next_bill_date',\n },\n buildFilters: async (query, ctx) => {\n const filters: Record<string, unknown> = {}\n // Spec F (F8): accounts with an overdue receivable \u2014 one indexed\n // DISTINCT over billing_invoices, intersected with any `id` filter.\n if (query.hasOverdue === true && ctx.auth?.tenantId) {\n const organizationIds = ctx.organizationIds ?? (ctx.selectedOrganizationId ? [ctx.selectedOrganizationId] : [])\n const em = ctx.container.resolve<EntityManager>('em')\n const overdueIds = await fetchOverdueAccountIds(em, { tenantId: ctx.auth.tenantId, organizationIds })\n filters.id = { $in: overdueIds.length > 0 ? overdueIds : [NO_MATCH_ID] }\n }\n // `?id=<uuid>` narrows the list to one row \u2014 the detail page\n // reads a single account through this filter.\n if (typeof query.id === 'string' && query.id) {\n const current = filters.id as { $in?: string[] } | undefined\n filters.id = current?.$in ? { $in: current.$in.includes(query.id) ? [query.id] : [NO_MATCH_ID] } : { $eq: query.id }\n }\n if (typeof query.customerId === 'string' && query.customerId) {\n filters.customer_id = { $eq: query.customerId }\n }\n if (typeof query.customerKind === 'string' && query.customerKind) {\n filters.customer_kind = { $eq: query.customerKind }\n }\n if (typeof query.currencyCode === 'string' && query.currencyCode) {\n filters.currency_code = { $eq: query.currencyCode.toUpperCase() }\n }\n if (typeof query.billCycle === 'string' && query.billCycle) {\n filters.bill_cycle = { $eq: query.billCycle }\n }\n if (typeof query.billingMode === 'string' && query.billingMode) {\n filters.billing_mode = { $eq: query.billingMode }\n }\n if (typeof query.isActive === 'boolean') {\n filters.is_active = { $eq: query.isActive }\n }\n if (typeof query.search === 'string' && query.search.trim()) {\n const pattern = `%${query.search.trim().replace(/[%_]/g, (m) => `\\\\${m}`)}%`\n filters.name = { $ilike: pattern }\n }\n return filters\n },\n },\n hooks: {\n // Spec F (F8): balance due / overdue per row \u2014 one GROUP BY for the page.\n afterList: async (payload: unknown, ctx: unknown) => {\n const items = ((payload as { items?: unknown[] } | null)?.items ?? []) as Array<Record<string, unknown>>\n if (items.length === 0) return\n const first = items[0]\n const tenantId = typeof first.tenant_id === 'string' ? first.tenant_id : null\n const organizationId = typeof first.organization_id === 'string' ? first.organization_id : null\n const ids = items.map((row) => row.id).filter((id): id is string => typeof id === 'string')\n const financials =\n tenantId && organizationId\n ? await fetchAccountFinancials(\n (ctx as { container: AwilixContainer }).container.resolve<EntityManager>('em'),\n { tenantId, organizationId },\n ids,\n )\n : new Map()\n for (const row of items) {\n const values = financialsOrZero(financials, String(row.id))\n row.balance_due = values.balance_due\n row.overdue_amount = values.overdue_amount\n }\n },\n },\n actions: {\n create: {\n commandId: 'billing.accounts.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingAccountCreateSchema.parse(scoped)\n },\n response: ({ result }) => ({ id: result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'billing.accounts.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingAccountUpdateSchema.parse(scoped)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'billing.accounts.delete',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n // The factory hands delete a `{ body, query }` envelope \u2014 the\n // id may arrive in either, depending on the caller.\n const envelope = (raw ?? {}) as {\n body?: Record<string, unknown>\n query?: Record<string, unknown>\n }\n const body = envelope.body ?? {}\n const query = envelope.query ?? {}\n const scoped = withScopedPayload(body, ctx, translate)\n const id =\n (typeof body.id === 'string' && body.id) ||\n (typeof query.id === 'string' && query.id) ||\n null\n if (!id) {\n throw new Error('id is required')\n }\n return {\n id,\n tenantId: (scoped as { tenantId?: string }).tenantId,\n organizationId: (scoped as { organizationId?: string }).organizationId,\n }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingAccount',\n pluralName: 'BillingAccounts',\n querySchema: billingAccountListQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billingAccountListItemSchema),\n create: {\n schema: billingAccountCreateSchema,\n responseSchema: defaultCreateResponseSchema,\n description:\n 'Creates a new billing account for a customer \u2014 a company or a person (`customerKind`, default `company`). ' +\n `\\`invoiceLanguage\\` must be a language supported by the invoice template (${INVOICE_LANGUAGE_CODES.map((c) => `\\`${c}\\``).join(', ')}; default \\`${DEFAULT_INVOICE_LANGUAGE}\\`).`,\n },\n update: {\n schema: billingAccountUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description:\n 'Updates a billing account. `currencyCode`, `customerId` and `customerKind` are immutable.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a billing account (sets `deleted_at`).',\n },\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,6 +3,7 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
4
|
import { BillingInvoice } from "../../../../data/entities.js";
|
|
5
5
|
import { getInvoicePdf } from "../../../../lib/pdf/invoicePdf.js";
|
|
6
|
+
import { INVOICE_LANGUAGE_CODES } from "../../../../lib/invoiceLanguages.js";
|
|
6
7
|
const metadata = {
|
|
7
8
|
GET: { requireAuth: true, requireFeatures: ["billing.invoice.view"] }
|
|
8
9
|
};
|
|
@@ -55,7 +56,7 @@ const openApi = {
|
|
|
55
56
|
{ name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } },
|
|
56
57
|
{ name: "download", in: "query", required: false, schema: { type: "string", enum: ["1"] } }
|
|
57
58
|
],
|
|
58
|
-
description:
|
|
59
|
+
description: `\`application/pdf\`. Posted documents return the PDF frozen at post (an attachment); drafts and test documents are rendered live with a DRAFT / TEST watermark and never stored. Labels follow the account invoice language (${INVOICE_LANGUAGE_CODES.join(" / ")}).`,
|
|
59
60
|
responses: [{ status: 200, description: "PDF document (`application/pdf`)" }],
|
|
60
61
|
errors: [{ status: 404, description: "Invoice not found in this organization" }]
|
|
61
62
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/billing/api/invoices/%5Bid%5D/pdf/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { BillingInvoice } from '../../../../data/entities'\nimport { getInvoicePdf } from '../../../../lib/pdf/invoicePdf'\n\n/**\n * `GET /api/billing/invoices/[id]/pdf` \u2014 the document PDF (Spec B).\n *\n * Posted documents: the frozen attachment (rendered and frozen on first\n * request if freezing failed at post). Drafts / test documents: a live\n * render with a DRAFT / TEST watermark, never stored. `?download=1` forces\n * an attachment disposition; otherwise the browser shows it inline.\n */\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.invoice.view'] },\n}\n\nexport async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = (auth as { orgId?: string }).orgId ?? null\n if (!organizationId) {\n return NextResponse.json({ error: 'Organization scope required' }, { status: 403 })\n }\n const { id } = await params\n const em = (container.resolve('em') as EntityManager).fork()\n const invoice = await em.findOne(BillingInvoice, {\n id,\n tenantId: auth.tenantId,\n organizationId,\n deletedAt: null,\n } as never)\n if (!invoice) {\n return NextResponse.json({ error: 'Invoice not found' }, { status: 404 })\n }\n const pdf = await getInvoicePdf(em, invoice)\n const download = new URL(req.url).searchParams.get('download') === '1'\n return new NextResponse(Buffer.from(pdf.bytes), {\n status: 200,\n headers: {\n 'Content-Type': 'application/pdf',\n 'Content-Length': String(pdf.bytes.byteLength),\n 'Content-Disposition': `${download ? 'attachment' : 'inline'}; filename=\"${pdf.fileName}\"`,\n // Drafts change with every edit; frozen documents never do.\n 'Cache-Control': pdf.frozen ? 'private, max-age=300' : 'no-store',\n 'X-Content-Type-Options': 'nosniff',\n },\n })\n } catch (err) {\n console.error('[billing/invoices/[id]/pdf.GET]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\nexport const openApi = {\n methods: {\n GET: {\n tags: ['Billing'],\n summary: 'Download a billing document PDF',\n parameters: [\n { name: 'id', in: 'path', required: true, schema: { type: 'string', format: 'uuid' } },\n { name: 'download', in: 'query', required: false, schema: { type: 'string', enum: ['1'] } },\n ],\n description:\n '`application/pdf`. Posted documents return the PDF frozen at post (an attachment); drafts ' +\n 'and test documents are rendered live with a DRAFT / TEST watermark and never stored. ' +\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAC/B,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { BillingInvoice } from '../../../../data/entities'\nimport { getInvoicePdf } from '../../../../lib/pdf/invoicePdf'\nimport { INVOICE_LANGUAGE_CODES } from '../../../../lib/invoiceLanguages'\n\n/**\n * `GET /api/billing/invoices/[id]/pdf` \u2014 the document PDF (Spec B).\n *\n * Posted documents: the frozen attachment (rendered and frozen on first\n * request if freezing failed at post). Drafts / test documents: a live\n * render with a DRAFT / TEST watermark, never stored. `?download=1` forces\n * an attachment disposition; otherwise the browser shows it inline.\n */\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.invoice.view'] },\n}\n\nexport async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = (auth as { orgId?: string }).orgId ?? null\n if (!organizationId) {\n return NextResponse.json({ error: 'Organization scope required' }, { status: 403 })\n }\n const { id } = await params\n const em = (container.resolve('em') as EntityManager).fork()\n const invoice = await em.findOne(BillingInvoice, {\n id,\n tenantId: auth.tenantId,\n organizationId,\n deletedAt: null,\n } as never)\n if (!invoice) {\n return NextResponse.json({ error: 'Invoice not found' }, { status: 404 })\n }\n const pdf = await getInvoicePdf(em, invoice)\n const download = new URL(req.url).searchParams.get('download') === '1'\n return new NextResponse(Buffer.from(pdf.bytes), {\n status: 200,\n headers: {\n 'Content-Type': 'application/pdf',\n 'Content-Length': String(pdf.bytes.byteLength),\n 'Content-Disposition': `${download ? 'attachment' : 'inline'}; filename=\"${pdf.fileName}\"`,\n // Drafts change with every edit; frozen documents never do.\n 'Cache-Control': pdf.frozen ? 'private, max-age=300' : 'no-store',\n 'X-Content-Type-Options': 'nosniff',\n },\n })\n } catch (err) {\n console.error('[billing/invoices/[id]/pdf.GET]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\nexport const openApi = {\n methods: {\n GET: {\n tags: ['Billing'],\n summary: 'Download a billing document PDF',\n parameters: [\n { name: 'id', in: 'path', required: true, schema: { type: 'string', format: 'uuid' } },\n { name: 'download', in: 'query', required: false, schema: { type: 'string', enum: ['1'] } },\n ],\n description:\n '`application/pdf`. Posted documents return the PDF frozen at post (an attachment); drafts ' +\n 'and test documents are rendered live with a DRAFT / TEST watermark and never stored. ' +\n `Labels follow the account invoice language (${INVOICE_LANGUAGE_CODES.join(' / ')}).`,\n responses: [{ status: 200, description: 'PDF document (`application/pdf`)' }],\n errors: [{ status: 404, description: 'Invoice not found in this organization' }],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAC/B,SAAS,qBAAqB;AAC9B,SAAS,8BAA8B;AAWhC,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACtE;AAEA,eAAsB,IAAI,KAAc,EAAE,OAAO,GAAwC;AACvF,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AACA,UAAM,iBAAkB,KAA4B,SAAS;AAC7D,QAAI,CAAC,gBAAgB;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,8BAA8B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpF;AACA,UAAM,EAAE,GAAG,IAAI,MAAM;AACrB,UAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,UAAM,UAAU,MAAM,GAAG,QAAQ,gBAAgB;AAAA,MAC/C;AAAA,MACA,UAAU,KAAK;AAAA,MACf;AAAA,MACA,WAAW;AAAA,IACb,CAAU;AACV,QAAI,CAAC,SAAS;AACZ,aAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1E;AACA,UAAM,MAAM,MAAM,cAAc,IAAI,OAAO;AAC3C,UAAM,WAAW,IAAI,IAAI,IAAI,GAAG,EAAE,aAAa,IAAI,UAAU,MAAM;AACnE,WAAO,IAAI,aAAa,OAAO,KAAK,IAAI,KAAK,GAAG;AAAA,MAC9C,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,kBAAkB,OAAO,IAAI,MAAM,UAAU;AAAA,QAC7C,uBAAuB,GAAG,WAAW,eAAe,QAAQ,eAAe,IAAI,QAAQ;AAAA;AAAA,QAEvF,iBAAiB,IAAI,SAAS,yBAAyB;AAAA,QACvD,0BAA0B;AAAA,MAC5B;AAAA,IACF,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,YAAQ,MAAM,mCAAmC,GAAG;AACpD,WAAO,aAAa,KAAK,EAAE,OAAO,iBAAiB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvE;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,SAAS;AAAA,IACP,KAAK;AAAA,MACH,MAAM,CAAC,SAAS;AAAA,MAChB,SAAS;AAAA,MACT,YAAY;AAAA,QACV,EAAE,MAAM,MAAM,IAAI,QAAQ,UAAU,MAAM,QAAQ,EAAE,MAAM,UAAU,QAAQ,OAAO,EAAE;AAAA,QACrF,EAAE,MAAM,YAAY,IAAI,SAAS,UAAU,OAAO,QAAQ,EAAE,MAAM,UAAU,MAAM,CAAC,GAAG,EAAE,EAAE;AAAA,MAC5F;AAAA,MACA,aACE,gOAE+C,uBAAuB,KAAK,KAAK,CAAC;AAAA,MACnF,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,mCAAmC,CAAC;AAAA,MAC5E,QAAQ,CAAC,EAAE,QAAQ,KAAK,aAAa,yCAAyC,CAAC;AAAA,IACjF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|