fix(crm): PR #603 review follow-ups + Outlook-proof email design

Addresses the maintainer's non-blocking review items + the Outlook email bug:
- invoice create: verify the chosen event belongs to the customer (only when
  the event has assignments; legacy unassigned events pass through).
- mark-paid + import: bound paidAt to [2000-01-01, now+30d] so a typo'd year
  can't silently drop a payment out of every cash-basis revenue window.
- customer routes: country_code now {min:2,max:2}+isAlpha+uppercase-normalize
  (was isString/max:2 — allowed '', '1', '!@'), matching the business-profile
  route.
- email transporter: close the previous instance before re-init (leak guard
  for a future pooled transport).
- scheduled-email tz: warn loudly when business_hours is set but the profile
  timezone is blank (was silently using the server/UTC tz).
- wrapEmailHtml: rebuild the chrome as inline-styled tables + bgcolor and
  inline the themed CTA button, so the design survives Outlook/Apple Mail
  stripping the head <style> (kept the <style> as progressive enhancement).
This commit is contained in:
Luca
2026-06-06 00:42:08 +02:00
parent b7fc86deef
commit a2b2d3fb31
4 changed files with 96 additions and 22 deletions
+16
View File
@@ -694,6 +694,22 @@ async function createInvoice(payload, adminId, trx = db) {
const customer = await trx('customer_accounts').where({ id: payload.customerAccountId }).first();
ensureCustomerCanBill(customer);
// PR #603 review follow-up #1 — when an invoice is attached to an event,
// make sure that event actually belongs to the chosen customer. Without
// this, a typo'd/copy-pasted eventId silently links the invoice to an
// unrelated event, producing misleading reporting links. Only enforced
// when the event HAS customer assignments (an event with none — e.g. a
// legacy import — is allowed through, since we can't prove a mismatch).
if (payload.eventId && await trx.schema.hasTable('event_customer_assignments')) {
const assignments = await trx('event_customer_assignments')
.where({ event_id: payload.eventId })
.select('customer_account_id');
if (assignments.length > 0 &&
!assignments.some(a => a.customer_account_id === payload.customerAccountId)) {
throw new AppError('The selected event is not assigned to this customer', 422, 'EVENT_CUSTOMER_MISMATCH');
}
}
// Accumulator intercept (migration 128). For customers in
// billing_cadence='monthly' OR 'manual' mode every createInvoice call
// APPENDS line items onto a single running draft instead of minting a