@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
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"billing.accounts.form.customer_kind": "Typ klienta",
|
|
69
69
|
"billing.accounts.form.cycle_anchor": "Zakotwiczenie cyklu",
|
|
70
70
|
"billing.accounts.form.cycle_anchor.month_hint": "1 — 28",
|
|
71
|
-
"billing.accounts.form.cycle_anchor.weekly_hint": "
|
|
71
|
+
"billing.accounts.form.cycle_anchor.weekly_hint": "mon | tue | … | sun",
|
|
72
72
|
"billing.accounts.form.error.address_required": "Adres na fakturze (linia 1 + miasto) jest wymagany",
|
|
73
73
|
"billing.accounts.form.error.country_invalid": "Kraj musi być 2- lub 3-literowym kodem ISO 3166",
|
|
74
74
|
"billing.accounts.form.error.currency_invalid": "Waluta musi być 3-literowym kodem ISO",
|
|
@@ -4,7 +4,7 @@ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
|
4
4
|
import type { SendEmailOptions } from '@open-mercato/shared/lib/email/send'
|
|
5
5
|
import { BillingAccount, BillingInvoice } from '../data/entities'
|
|
6
6
|
import { emitBillingEvent } from '../events'
|
|
7
|
-
import {
|
|
7
|
+
import { resolveInvoiceLanguage } from './invoiceLanguages'
|
|
8
8
|
import { formatIban } from './iban'
|
|
9
9
|
import { documentLocale, formatDocumentDate, formatDocumentMoney, type DocumentTranslate } from './pdf/invoiceModel'
|
|
10
10
|
import { getInvoicePdf, loadDocumentTranslator, type TranslatorLoader } from './pdf/invoicePdf'
|
|
@@ -148,10 +148,9 @@ export async function sendInvoice(
|
|
|
148
148
|
})
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
const language =
|
|
152
|
-
(typeof buyer.invoice_language === 'string' && buyer.invoice_language) ||
|
|
153
|
-
|
|
154
|
-
DEFAULT_INVOICE_LANGUAGE
|
|
151
|
+
const language = resolveInvoiceLanguage(
|
|
152
|
+
(typeof buyer.invoice_language === 'string' && buyer.invoice_language) || account?.invoiceLanguage,
|
|
153
|
+
)
|
|
155
154
|
const translate = await (deps.loadTranslator ?? loadDocumentTranslator)(language)
|
|
156
155
|
const seller = (invoice.sellerSnapshot ?? {}) as Record<string, unknown>
|
|
157
156
|
const email = buildInvoiceEmail({
|
|
@@ -1,24 +1,35 @@
|
|
|
1
|
+
import type { Locale } from '@open-mercato/shared/lib/i18n/config'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Invoice languages the built-in billing document template can render.
|
|
3
5
|
*
|
|
4
|
-
* Single source of truth for the account form's language picklist
|
|
5
|
-
* `invoiceLanguage` validator
|
|
6
|
-
*
|
|
6
|
+
* Single source of truth for the account form's language picklist, the
|
|
7
|
+
* `invoiceLanguage` validator, the `Intl` locale used for money / dates on
|
|
8
|
+
* documents and emails, and the dictionary the document translator loads.
|
|
9
|
+
* Adding a language means adding `i18n/<code>.json` AND one entry here — the
|
|
10
|
+
* dictionary-parity test fails if either is missing.
|
|
11
|
+
*
|
|
12
|
+
* Every `code` must be a UI locale the framework ships (`Locale`), because the
|
|
13
|
+
* document translator loads the regular module dictionary for it. The
|
|
14
|
+
* `satisfies` clause below turns a code the framework does not ship into a
|
|
15
|
+
* compile error.
|
|
7
16
|
*
|
|
8
17
|
* `label` is the language's own name (endonym), so it reads correctly no
|
|
9
18
|
* matter which UI locale the operator uses — it is intentionally not routed
|
|
10
|
-
* through i18n.
|
|
19
|
+
* through i18n. Order: default first, then product priority.
|
|
11
20
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* selectable.
|
|
21
|
+
* Specs: .ai/specs/2026-11-14-billing-account-workspace.md (D22),
|
|
22
|
+
* .ai/specs/2026-11-15-billing-multi-language.md. Narrowing the list per
|
|
23
|
+
* organization (`BillingSettings.enabled_invoice_languages`) arrives with the
|
|
24
|
+
* standalone-ledger spec; until then every supported language is selectable.
|
|
16
25
|
*/
|
|
17
26
|
|
|
18
27
|
export const SUPPORTED_INVOICE_LANGUAGES = [
|
|
19
|
-
{ code: 'en', label: 'English' },
|
|
20
|
-
{ code: '
|
|
21
|
-
|
|
28
|
+
{ code: 'en', label: 'English', intlLocale: 'en-GB' },
|
|
29
|
+
{ code: 'es', label: 'Español', intlLocale: 'es-ES' },
|
|
30
|
+
{ code: 'de', label: 'Deutsch', intlLocale: 'de-DE' },
|
|
31
|
+
{ code: 'pl', label: 'Polski', intlLocale: 'pl-PL' },
|
|
32
|
+
] as const satisfies ReadonlyArray<{ code: Locale; label: string; intlLocale: string }>
|
|
22
33
|
|
|
23
34
|
export type InvoiceLanguageCode = (typeof SUPPORTED_INVOICE_LANGUAGES)[number]['code']
|
|
24
35
|
|
|
@@ -32,3 +43,15 @@ export const DEFAULT_INVOICE_LANGUAGE: InvoiceLanguageCode = 'en'
|
|
|
32
43
|
export function isSupportedInvoiceLanguage(value: string): value is InvoiceLanguageCode {
|
|
33
44
|
return (INVOICE_LANGUAGE_CODES as readonly string[]).includes(value)
|
|
34
45
|
}
|
|
46
|
+
|
|
47
|
+
/** Normalizes a stored / requested language to a supported code (`' PL '` → `pl`), else the default. */
|
|
48
|
+
export function resolveInvoiceLanguage(value: string | null | undefined): InvoiceLanguageCode {
|
|
49
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
50
|
+
return isSupportedInvoiceLanguage(normalized) ? normalized : DEFAULT_INVOICE_LANGUAGE
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The `Intl` locale documents in this language format money and dates with. */
|
|
54
|
+
export function invoiceIntlLocale(language: string | null | undefined): string {
|
|
55
|
+
const code = resolveInvoiceLanguage(language)
|
|
56
|
+
return SUPPORTED_INVOICE_LANGUAGES.find((l) => l.code === code)?.intlLocale ?? 'en-GB'
|
|
57
|
+
}
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* "corrects" row is added to the header.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
import { invoiceIntlLocale } from '../invoiceLanguages'
|
|
16
|
+
|
|
17
|
+
export type DocumentTranslate =(key: string, fallback: string) => string
|
|
16
18
|
|
|
17
19
|
export type PdfAddress = {
|
|
18
20
|
line1?: string | null
|
|
@@ -91,10 +93,9 @@ export type InvoiceDocumentModel = {
|
|
|
91
93
|
pageLabel: (page: number, pages: number) => string
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
/** `Intl` locale for a document language — derived from the language registry. */
|
|
96
97
|
export function documentLocale(language: string): string {
|
|
97
|
-
return
|
|
98
|
+
return invoiceIntlLocale(language)
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
function toDate(value: Date | string | null | undefined): Date | null {
|
|
@@ -3,7 +3,7 @@ import { BillingAccount, BillingInvoice, BillingInvoiceLine } from '../../data/e
|
|
|
3
3
|
import { billingEntityIds } from '../../data/entityIds'
|
|
4
4
|
import { buildSellerSnapshot, getBillingSettings } from '../billingSettings'
|
|
5
5
|
import { buildBuyerSnapshot } from '../documentParties'
|
|
6
|
-
import {
|
|
6
|
+
import { resolveInvoiceLanguage } from '../invoiceLanguages'
|
|
7
7
|
import { buildInvoiceDocumentModel, type DocumentTranslate, type PdfAddress } from './invoiceModel'
|
|
8
8
|
import { renderInvoicePdf, type PdfLogo } from './invoiceTemplate'
|
|
9
9
|
import { attachmentIdFromUrl, readAttachment, storeAttachment } from './attachmentStore'
|
|
@@ -25,11 +25,15 @@ import { attachmentIdFromUrl, readAttachment, storeAttachment } from './attachme
|
|
|
25
25
|
|
|
26
26
|
export type TranslatorLoader = (language: string) => Promise<DocumentTranslate>
|
|
27
27
|
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Billing i18n for a document language. Any supported invoice language loads
|
|
30
|
+
* its own dictionary (registry in `../invoiceLanguages`); anything else falls
|
|
31
|
+
* back to the default language.
|
|
32
|
+
*/
|
|
29
33
|
export const loadDocumentTranslator: TranslatorLoader = async (language) => {
|
|
30
34
|
const { loadDictionary } = await import('@open-mercato/shared/lib/i18n/server')
|
|
31
35
|
const { createFallbackTranslator } = await import('@open-mercato/shared/lib/i18n/translate')
|
|
32
|
-
const locale = language
|
|
36
|
+
const locale = resolveInvoiceLanguage(language)
|
|
33
37
|
const translate = createFallbackTranslator(await loadDictionary(locale))
|
|
34
38
|
return (key, fallback) => translate(key, fallback)
|
|
35
39
|
}
|
|
@@ -93,7 +97,7 @@ export async function renderBillingDocument(
|
|
|
93
97
|
{ orderBy: { lineNumber: 'asc' } as never },
|
|
94
98
|
)
|
|
95
99
|
const { seller, buyer } = await resolveParties(em, invoice)
|
|
96
|
-
const language = str(buyer.invoice_language)
|
|
100
|
+
const language = resolveInvoiceLanguage(str(buyer.invoice_language))
|
|
97
101
|
const translate = await (deps.loadTranslator ?? loadDocumentTranslator)(language)
|
|
98
102
|
|
|
99
103
|
let correctsNumber: string | null = null
|
|
@@ -27,7 +27,7 @@ const SMALL_SIZE = 8
|
|
|
27
27
|
const LINE_GAP = 3
|
|
28
28
|
|
|
29
29
|
// # | description | qty | unit net | VAT % | net | VAT | gross (sums to CONTENT_WIDTH)
|
|
30
|
-
const TABLE_COLUMNS = [24, 179, 30, 62, 36, 62, 56, 66.28]
|
|
30
|
+
export const TABLE_COLUMNS = [24, 179, 30, 62, 36, 62, 56, 66.28]
|
|
31
31
|
|
|
32
32
|
const TEXT = rgb(0.12, 0.12, 0.14)
|
|
33
33
|
const MUTED = rgb(0.42, 0.42, 0.46)
|
|
@@ -51,7 +51,7 @@ export function sanitizeForFont(text: string, charset: Set<number>): string {
|
|
|
51
51
|
return out
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function wrap(text: string, font: PDFFont, size: number, width: number): string[] {
|
|
54
|
+
export function wrap(text: string, font: PDFFont, size: number, width: number): string[] {
|
|
55
55
|
const lines: string[] = []
|
|
56
56
|
for (const paragraph of text.split('\n')) {
|
|
57
57
|
let current = ''
|