From a2b2d3fb313f347bac505b5d1846dce2b679ce55 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Sat, 6 Jun 2026 00:42:08 +0200 Subject: [PATCH] fix(crm): PR #603 review follow-ups + Outlook-proof email design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 - -
-
- - - -
-
+ + + + + + `; } @@ -886,7 +916,18 @@ async function getScheduledEmailConfig() { const schedule = normaliseSchedule(profile.business_hours); let timezone = (profile.timezone || '').trim(); - if (!timezone) timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; + if (!timezone) { + // PR #603 review follow-up #4 — business hours are configured but the + // profile timezone is blank, so we fall back to the SERVER's tz (usually + // UTC on a Docker host). That silently shifts every business-hours + // calculation. Warn loudly so the admin sets business_profile.timezone. + timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; + logger.warn( + 'Scheduled-email business hours are set but business_profile.timezone is blank — ' + + `falling back to the server timezone (${timezone}). Set the profile timezone ` + + 'so business-hours snapping uses your local time, not the server\'s.', + ); + } // Reject a bogus tz before it reaches Intl in the snap helper. try { new Intl.DateTimeFormat('en-US', { timeZone: timezone }); diff --git a/backend/src/services/invoiceService.js b/backend/src/services/invoiceService.js index 91a92e68..4f708097 100644 --- a/backend/src/services/invoiceService.js +++ b/backend/src/services/invoiceService.js @@ -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