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
125 lines
4.4 KiB
JavaScript
125 lines
4.4 KiB
JavaScript
/**
|
|
* Tests for the ISO 13616 IBAN validator.
|
|
*
|
|
* Reference IBANs sourced from the SWIFT IBAN Registry "Example" section
|
|
* — they are publicly published sample values used by every IBAN
|
|
* implementation as test vectors. NOT real account numbers.
|
|
*/
|
|
const { validateIban, _internal } = require('../../src/utils/iban');
|
|
|
|
describe('validateIban', () => {
|
|
it('accepts a canonical Swiss IBAN', () => {
|
|
const out = validateIban('CH9300762011623852957');
|
|
expect(out.valid).toBe(true);
|
|
expect(out.normalized).toBe('CH9300762011623852957');
|
|
expect(out.reason).toBeUndefined();
|
|
});
|
|
|
|
it('accepts a Liechtenstein IBAN', () => {
|
|
expect(validateIban('LI21088100002324013AA').valid).toBe(true);
|
|
});
|
|
|
|
it('accepts a German IBAN', () => {
|
|
expect(validateIban('DE89370400440532013000').valid).toBe(true);
|
|
});
|
|
|
|
it('accepts an Austrian IBAN', () => {
|
|
expect(validateIban('AT611904300234573201').valid).toBe(true);
|
|
});
|
|
|
|
it('accepts a British IBAN with alphanumeric BBAN', () => {
|
|
expect(validateIban('GB82WEST12345698765432').valid).toBe(true);
|
|
});
|
|
|
|
it('normalises spaces and lowercase input', () => {
|
|
const out = validateIban(' ch93 0076 2011 6238 5295 7 ');
|
|
expect(out.valid).toBe(true);
|
|
expect(out.normalized).toBe('CH9300762011623852957');
|
|
});
|
|
|
|
it('normalises mixed-case input', () => {
|
|
const out = validateIban('ch9300762011623852957');
|
|
expect(out.valid).toBe(true);
|
|
expect(out.normalized).toBe('CH9300762011623852957');
|
|
});
|
|
|
|
it('rejects an empty / null / undefined value', () => {
|
|
expect(validateIban('').reason).toBe('EMPTY');
|
|
expect(validateIban(' ').reason).toBe('EMPTY');
|
|
expect(validateIban(null).reason).toBe('EMPTY');
|
|
expect(validateIban(undefined).reason).toBe('EMPTY');
|
|
});
|
|
|
|
it('rejects a malformed string (numbers in the country slot)', () => {
|
|
const out = validateIban('12930076201162385295');
|
|
expect(out.valid).toBe(false);
|
|
expect(out.reason).toBe('FORMAT');
|
|
});
|
|
|
|
it('rejects a too-short string', () => {
|
|
expect(validateIban('CH93').reason).toBe('FORMAT');
|
|
});
|
|
|
|
it('rejects a too-long string (over 34 chars)', () => {
|
|
// 35 chars: pads beyond the ISO 13616 max
|
|
expect(validateIban('CH9300762011623852957XXXXXXXXXXXXXX').reason).toBe('FORMAT');
|
|
});
|
|
|
|
it('rejects a known-country IBAN with the wrong length', () => {
|
|
// CH must be 21 chars; this one is 22.
|
|
const out = validateIban('CH9300762011623852957X');
|
|
expect(out.valid).toBe(false);
|
|
expect(out.reason).toBe('LENGTH');
|
|
});
|
|
|
|
it('rejects an IBAN with a broken checksum', () => {
|
|
// Same shape, last digit altered.
|
|
const out = validateIban('CH9300762011623852950');
|
|
expect(out.valid).toBe(false);
|
|
expect(out.reason).toBe('CHECKSUM');
|
|
});
|
|
|
|
it('rejects an IBAN with internally invalid characters', () => {
|
|
expect(validateIban('CH93007620!1623852957').reason).toBe('FORMAT');
|
|
});
|
|
|
|
it('accepts an unknown-country IBAN that meets the generic length range', () => {
|
|
// Made-up country code "ZZ" — not in IBAN_LENGTHS but the
|
|
// structural regex passes if length is in [15, 34] and the
|
|
// checksum holds. Build a checksum-valid string:
|
|
//
|
|
// Format: ZZ + check + BBAN. We don't have a real ZZ template
|
|
// so this test just confirms unknown country codes route
|
|
// through the fallback length check rather than failing on
|
|
// LENGTH outright. A checksum-failing ZZ value will hit
|
|
// CHECKSUM, not LENGTH, which is the assertion below.
|
|
const out = validateIban('ZZ00ABCDEFGHIJKLMNOP');
|
|
expect(out.valid).toBe(false);
|
|
expect(out.reason).toBe('CHECKSUM'); // not LENGTH
|
|
});
|
|
});
|
|
|
|
describe('mod97', () => {
|
|
it('returns 1 for the canonical CH test vector', () => {
|
|
expect(_internal.mod97('CH9300762011623852957')).toBe(1);
|
|
});
|
|
|
|
it('returns something other than 1 for a tampered IBAN', () => {
|
|
expect(_internal.mod97('CH9300762011623852950')).not.toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('IBAN_LENGTHS table', () => {
|
|
it('has the expected lengths for the most common European countries', () => {
|
|
// Sanity check that the table didn't drift if someone edits it.
|
|
expect(_internal.IBAN_LENGTHS.CH).toBe(21);
|
|
expect(_internal.IBAN_LENGTHS.DE).toBe(22);
|
|
expect(_internal.IBAN_LENGTHS.AT).toBe(20);
|
|
expect(_internal.IBAN_LENGTHS.LI).toBe(21);
|
|
expect(_internal.IBAN_LENGTHS.FR).toBe(27);
|
|
expect(_internal.IBAN_LENGTHS.IT).toBe(27);
|
|
expect(_internal.IBAN_LENGTHS.GB).toBe(22);
|
|
expect(_internal.IBAN_LENGTHS.NL).toBe(18);
|
|
});
|
|
});
|