d543949188
Brings in the full backend CRM stack on top of the consolidated
migration (60abe8c).
Services (CRM)
- quoteService — full lifecycle (draft → sent → accepted → converted
to event/invoice), Skonto + Storno + reissue paths
- invoiceService — spawnInstallmentInvoices, updateInstallmentPlan,
monthly-billing accumulator, payment-check tokens, dunning ladder
- contractService — block-composable contract editor, in-browser
signature flow, wet-PDF upload path, integrity check, audit trail
- customerHoursService — per-entry locking, billing integration
- dealsService — cross-document lineage (deal_uuid)
- taxReportService — quarterly aggregates + CSV/PDF export
- eventReminderService — pre-event customer reminder cron pass
- _renderContext — shared issuer/recipient blocks across PDF types
- pdfService extensions — custom-font registration, font picker
Routes (admin + public)
- adminQuotes, adminInvoices, adminContracts, adminCalendar,
adminDeals, adminTaxReport, adminDev, adminBusinessProfile
- publicQuotes (accept/decline), publicContracts (sign),
publicPaymentCheck
- Extensions on adminEvents, adminCustomers, adminSettings,
adminEmail, adminFeatureFlags, adminThumbnails, adminPhotos,
adminCategories, adminUsers, adminArchives, adminDashboard
- server.js wires the new mounts (kept upstream's noStoreCache on
customer routes per 3-way merge)
Utilities
- schemaCache (cached hasColumn lookups across services)
- documentSequences (atomic gap-free numbering — §14 UStG)
- safePath (path-containment guards at fs stream boundaries)
- clientIp (sanctioned XFF reader for audit logs)
- publicTokenGuards (pre-multer token validation + attempt counters)
- numericHelpers (ensureInt / ensureNumber consolidation)
- dateFormatter (formatShortDate + dateInputLang)
- dbCompat extensions, iban + pdfFilename helpers, resolveLogoFile
Infrastructure
- Bundled PDF fonts (Comic-Neue / IBM-Plex-Sans / Inter / Jost /
Montserrat / Noto-Sans / Playfair-Display / Poppins)
- Backend package.json + lock updates (pdfkit, signature_pad,
qrcode, et al.)
- Sample storage layout under storage/business-docs/quote/
Tests
- 14 new test files covering quote/invoice/contract lifecycle,
installment plan reshape, line-item hierarchy, customer hours,
payment check, tax report PDF, IBAN parsing, filename sanitiser
96 lines
3.4 KiB
JavaScript
96 lines
3.4 KiB
JavaScript
/**
|
||
* Pure-function tests for the PDF rendering helpers. These are the
|
||
* functions that DON'T touch PDFKit / DB — formatting, salutation
|
||
* routing, EPC payload construction.
|
||
*
|
||
* The helpers aren't directly exported from pdfService.js (it
|
||
* exports renderQuoteToBuffer / renderInvoiceToBuffer); we reach
|
||
* them via `_internal` which the module already exposes for tests.
|
||
*/
|
||
const pdfService = require('../../src/services/pdfService');
|
||
const { formatMinor, formatDate, t } = pdfService._internal;
|
||
|
||
describe('formatMinor', () => {
|
||
it('formats CHF cents with 2 decimals (123456 minor = 1234.56 major)', () => {
|
||
// de-CH uses ’ (U+2019) as the thousands separator.
|
||
expect(formatMinor(123456, 'CHF', 'de-CH')).toMatch(/1[’',\u2019]?234\.56/);
|
||
});
|
||
|
||
it('formats large amounts with thousands separators', () => {
|
||
// 12345600 minor units = 123,456.00 major; the separator
|
||
// varies by locale (de-CH = U+2019, en-GB = ',').
|
||
expect(formatMinor(12345600, 'CHF', 'de-CH')).toMatch(/123[’',\u2019]456\.00/);
|
||
});
|
||
|
||
it('returns 0,00 for zero or null', () => {
|
||
expect(formatMinor(0, 'CHF', 'de-CH')).toMatch(/0[,.]00/);
|
||
expect(formatMinor(null, 'CHF', 'de-CH')).toMatch(/0[,.]00/);
|
||
});
|
||
|
||
it('returns 2-decimal output regardless of locale', () => {
|
||
expect(formatMinor(99, 'EUR', 'en-GB')).toMatch(/0[.,]99/);
|
||
});
|
||
});
|
||
|
||
describe('formatDate', () => {
|
||
// formatDate now respects ctx.dateFormat (object with `format`
|
||
// key) — when omitted defaults to DD.MM.YYYY.
|
||
it('defaults to DD.MM.YYYY when no format passed', () => {
|
||
expect(formatDate('2026-04-19')).toBe('19.04.2026');
|
||
});
|
||
|
||
it('honors the configured DD/MM/YYYY format', () => {
|
||
expect(formatDate('2026-04-19', { format: 'DD/MM/YYYY' })).toBe('19/04/2026');
|
||
});
|
||
|
||
it('honors the configured MM/DD/YYYY format', () => {
|
||
expect(formatDate('2026-04-19', { format: 'MM/DD/YYYY' })).toBe('04/19/2026');
|
||
});
|
||
|
||
it('honors ISO YYYY-MM-DD', () => {
|
||
expect(formatDate('2026-04-19', { format: 'YYYY-MM-DD' })).toBe('2026-04-19');
|
||
});
|
||
|
||
it('returns empty string on empty input', () => {
|
||
expect(formatDate('')).toBe('');
|
||
expect(formatDate(null)).toBe('');
|
||
expect(formatDate(undefined)).toBe('');
|
||
});
|
||
|
||
it('returns empty string on invalid input rather than throwing', () => {
|
||
expect(formatDate('not-a-date')).toBe('');
|
||
});
|
||
|
||
it('accepts Date objects', () => {
|
||
expect(formatDate(new Date('2026-04-19T12:00:00Z'))).toMatch(/^(19|20)\.0[34]\.2026$/);
|
||
});
|
||
});
|
||
|
||
describe('t (i18n lookup)', () => {
|
||
it('returns the EN value for an EN-only locale', () => {
|
||
expect(t('en', 'invoice_title')).toBe('Invoice');
|
||
expect(t('en', 'quote_title')).toBe('Quote');
|
||
});
|
||
|
||
it('returns the DE value for de locale', () => {
|
||
expect(t('de', 'invoice_title')).toBe('Rechnung');
|
||
expect(t('de', 'quote_title')).toBe('Angebot');
|
||
});
|
||
|
||
it('falls back to EN for unknown locales', () => {
|
||
expect(t('xx', 'invoice_title')).toBe('Invoice');
|
||
});
|
||
|
||
it('substitutes named tokens like {percent}', () => {
|
||
const out = t('en', 'skonto_phrase', { percent: 3, days: 5 });
|
||
expect(out).toMatch(/3% discount if paid within 5 working days\./);
|
||
});
|
||
|
||
it('falls back to EN when the key is missing on the requested locale', () => {
|
||
// page_of is seeded on all locales — pick something that
|
||
// exists on EN with a substitution.
|
||
const out = t('zz', 'page_of', { current: 1, total: 3 });
|
||
expect(out).toBe('Page 1 of 3');
|
||
});
|
||
});
|