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
97 lines
3.3 KiB
JavaScript
97 lines
3.3 KiB
JavaScript
/**
|
||
* schemaCache — process-local memoisation for `db.schema.hasColumn`.
|
||
*
|
||
* **Why this exists**
|
||
*
|
||
* The CRM services on `feat/crm` are riddled with `hasColumn` guards
|
||
* because the schema has been drifting fast (every doc-feature
|
||
* migration adds a column that older installs may not yet have).
|
||
* Each call hits information_schema (Postgres) or sqlite_master
|
||
* (SQLite). On hot paths — `recordCustomerSignature`,
|
||
* `recordAdminCountersignature`, `getContractById`, the monthly
|
||
* billing pass — we issue 4–8 hasColumn checks per request, all
|
||
* for columns whose presence cannot change at runtime.
|
||
*
|
||
* The audit flagged this as a perf medium. Caching is safe because:
|
||
*
|
||
* 1. Schema-changing operations (migrations, `ALTER TABLE`) only
|
||
* run at boot via `run-migrations-safe.js`, BEFORE any service
|
||
* module accepts traffic. The cache is populated lazily after
|
||
* boot finishes, so the entries reflect post-migration state.
|
||
*
|
||
* 2. The Node process is the only schema authority. There's no
|
||
* sibling process sneaking in `ALTER TABLE` while we serve
|
||
* requests.
|
||
*
|
||
* 3. If a future migration path needs to run mid-flight, it can
|
||
* call `invalidateSchemaCache()` after the schema change.
|
||
*
|
||
* **What we cache**
|
||
*
|
||
* Just the boolean answer to `(table, column)`. A miss means the
|
||
* column doesn't exist on this install; a hit means it does. The
|
||
* cache key is `${table}.${column}`. There's no TTL — the entry is
|
||
* valid for the lifetime of the Node process.
|
||
*
|
||
* **What we DON'T cache**
|
||
*
|
||
* Negative results from `hasTable` failures (the table simply isn't
|
||
* there) — those go through the underlying call each time. That
|
||
* scenario is exceptional (table truly missing during a half-applied
|
||
* migration window) and we want it to surface, not get masked by a
|
||
* stale cache.
|
||
*
|
||
* **API**
|
||
*
|
||
* const { hasColumnCached, invalidateSchemaCache } = require('../utils/schemaCache');
|
||
* if (await hasColumnCached('contracts', 'signed_pdf_render_failed_at')) {
|
||
* ...
|
||
* }
|
||
*
|
||
* Drop-in replacement for `db.schema.hasColumn(...)` calls. The
|
||
* existing helper signature returns a Promise<boolean> so async
|
||
* call-sites need no shape change.
|
||
*/
|
||
|
||
const { db } = require('../database/db');
|
||
|
||
const cache = new Map();
|
||
|
||
async function hasColumnCached(table, column) {
|
||
const key = `${table}.${column}`;
|
||
if (cache.has(key)) return cache.get(key);
|
||
// Resolve via the underlying schema API. We deliberately don't
|
||
// catch errors here — if the call throws (e.g. DB connection lost
|
||
// mid-boot), the error surfaces to the caller exactly as it would
|
||
// have without the cache.
|
||
const present = await db.schema.hasColumn(table, column);
|
||
cache.set(key, present);
|
||
return present;
|
||
}
|
||
|
||
/**
|
||
* Drop every cached entry. Call this after a runtime schema change
|
||
* (rare — only the dev tooling does this today). Safe to call any
|
||
* time; the next hasColumnCached lookup will re-resolve.
|
||
*/
|
||
function invalidateSchemaCache() {
|
||
cache.clear();
|
||
}
|
||
|
||
/**
|
||
* Drop entries for a single table. Useful when only one table was
|
||
* altered and other tables' caches are still valid.
|
||
*/
|
||
function invalidateSchemaCacheForTable(table) {
|
||
const prefix = `${table}.`;
|
||
for (const k of cache.keys()) {
|
||
if (k.startsWith(prefix)) cache.delete(k);
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
hasColumnCached,
|
||
invalidateSchemaCache,
|
||
invalidateSchemaCacheForTable,
|
||
};
|