Files
picpeak/backend/__tests__/services/pdfService.helpers.test.js
T
Luca d543949188 feat(crm): backend code — services + routes + utilities + tests
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
2026-05-26 18:18:51 +02:00

96 lines
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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');
});
});