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
122 lines
3.6 KiB
JavaScript
122 lines
3.6 KiB
JavaScript
/**
|
|
* Pure-function tests for the PDF filename builder used on every
|
|
* quote / invoice download endpoint + the PDF's internal Title
|
|
* metadata. No mocks needed — all behavior is deterministic.
|
|
*/
|
|
const { buildPdfFilename, sanitiseSegment, customerLabel } = require('../../src/utils/pdfFilename');
|
|
|
|
describe('sanitiseSegment', () => {
|
|
it('returns empty string for null/undefined', () => {
|
|
expect(sanitiseSegment(null)).toBe('');
|
|
expect(sanitiseSegment(undefined)).toBe('');
|
|
expect(sanitiseSegment('')).toBe('');
|
|
});
|
|
|
|
it('replaces filesystem-hostile characters with "-"', () => {
|
|
expect(sanitiseSegment('a/b\\c:d*e?f"g<h>i|j')).toBe('a-b-c-d-e-f-g-h-i-j');
|
|
});
|
|
|
|
it('collapses spaces into single "-"', () => {
|
|
expect(sanitiseSegment('ACME GmbH AG')).toBe('ACME-GmbH-AG');
|
|
});
|
|
|
|
it('collapses repeat dashes', () => {
|
|
expect(sanitiseSegment('a-----b')).toBe('a-b');
|
|
});
|
|
|
|
it('trims leading + trailing dashes/dots', () => {
|
|
expect(sanitiseSegment('--..--Hello..--..')).toBe('Hello');
|
|
});
|
|
|
|
it('preserves non-ASCII letters', () => {
|
|
expect(sanitiseSegment('Müller & Söhne')).toBe('Müller-&-Söhne');
|
|
});
|
|
|
|
it('caps length at 80 chars by default', () => {
|
|
const long = 'a'.repeat(120);
|
|
expect(sanitiseSegment(long)).toHaveLength(80);
|
|
});
|
|
|
|
it('honors custom maxLen', () => {
|
|
expect(sanitiseSegment('abcdefghij', 5)).toBe('abcde');
|
|
});
|
|
});
|
|
|
|
describe('customerLabel', () => {
|
|
it('prefers company_name over person name', () => {
|
|
expect(customerLabel({
|
|
company_name: 'ACME GmbH',
|
|
first_name: 'Luca', last_name: 'Bresch',
|
|
})).toBe('ACME-GmbH');
|
|
});
|
|
|
|
it('falls back to first + last when company_name is empty', () => {
|
|
expect(customerLabel({
|
|
company_name: '',
|
|
first_name: 'Luca', last_name: 'Bresch',
|
|
})).toBe('Luca-Bresch');
|
|
});
|
|
|
|
it('falls back to display_name when no company + no person', () => {
|
|
expect(customerLabel({
|
|
display_name: 'Luca B.',
|
|
})).toBe('Luca-B');
|
|
});
|
|
|
|
it('falls back to email local-part as a last resort', () => {
|
|
expect(customerLabel({
|
|
email: 'luca@bresch.cc',
|
|
})).toBe('luca');
|
|
});
|
|
|
|
it('uses "customer" when everything is missing', () => {
|
|
expect(customerLabel({})).toBe('customer');
|
|
expect(customerLabel(null)).toBe('customer');
|
|
});
|
|
|
|
it('trims whitespace before evaluating truthiness', () => {
|
|
// company_name = " " should NOT trigger the company branch.
|
|
expect(customerLabel({
|
|
company_name: ' ',
|
|
first_name: 'Luca', last_name: 'Bresch',
|
|
})).toBe('Luca-Bresch');
|
|
});
|
|
});
|
|
|
|
describe('buildPdfFilename', () => {
|
|
const customer = { company_name: 'ACME GmbH' };
|
|
|
|
it('builds "<docNumber>_<customer>.pdf" for a regular invoice', () => {
|
|
expect(buildPdfFilename({
|
|
docNumber: 'R-2026-0001',
|
|
customer,
|
|
})).toBe('R-2026-0001_ACME-GmbH.pdf');
|
|
});
|
|
|
|
it('falls back to the fallback when docNumber is null (preview)', () => {
|
|
expect(buildPdfFilename({
|
|
docNumber: null,
|
|
customer,
|
|
fallback: 'invoice-preview',
|
|
})).toBe('invoice-preview_ACME-GmbH.pdf');
|
|
});
|
|
|
|
it('uses "document" when both docNumber + fallback are absent', () => {
|
|
expect(buildPdfFilename({ customer })).toBe('document_ACME-GmbH.pdf');
|
|
});
|
|
|
|
it('sanitises the customer half too', () => {
|
|
expect(buildPdfFilename({
|
|
docNumber: 'R-2026-0001',
|
|
customer: { company_name: 'Bad/Name:Inc.' },
|
|
})).toBe('R-2026-0001_Bad-Name-Inc.pdf');
|
|
});
|
|
|
|
it('always ends with .pdf', () => {
|
|
expect(buildPdfFilename({
|
|
docNumber: 'R-2026-0001',
|
|
customer: {},
|
|
})).toMatch(/\.pdf$/);
|
|
});
|
|
});
|