fix(audit): wire reminder defaults into createInterest; doc branding gap (R2-H15/H16)
R2-H16: /admin/reminders persisted defaultEnabled + defaultDays to system_settings but createInterest ignored them — every new interest defaulted to reminderEnabled=false regardless. The validator now treats reminderEnabled / reminderDays as optional (no default false), and createInterest falls back to getPortReminderConfig(portId) when the caller omits them. Explicit false / null still opts out. R2-H15: branding admin (/admin/branding) saves 5 settings that no code reads — the email templates and BrandedAuthShell hardcode Port Nimara branding. Wiring it end-to-end is a multi-template refactor; documented the gap inline above getPortBrandingConfig with a step-by-step wire-up plan so future devs don't think it's done. The reminder-digest scheduler (digestEnabled/digestTime/digestTimezone) remains unimplemented — needs a new BullMQ recurring job that batches pending reminders into per-user/per-port digest emails. Out of scope for this audit pass. 1175/1175 vitest passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { yachts } from '@/lib/db/schema/yachts';
|
|||||||
import { companyMemberships } from '@/lib/db/schema/companies';
|
import { companyMemberships } from '@/lib/db/schema/companies';
|
||||||
import { tags } from '@/lib/db/schema/system';
|
import { tags } from '@/lib/db/schema/system';
|
||||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||||
|
import { getPortReminderConfig } from '@/lib/services/port-config';
|
||||||
import { NotFoundError, ConflictError, ValidationError } from '@/lib/errors';
|
import { NotFoundError, ConflictError, ValidationError } from '@/lib/errors';
|
||||||
import { emitToRoom } from '@/lib/socket/server';
|
import { emitToRoom } from '@/lib/socket/server';
|
||||||
import { setEntityTags } from '@/lib/services/entity-tags.helper';
|
import { setEntityTags } from '@/lib/services/entity-tags.helper';
|
||||||
@@ -438,12 +439,21 @@ export async function createInterest(portId: string, data: CreateInterestInput,
|
|||||||
data.yachtId,
|
data.yachtId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Per-port reminder defaults — applied only when the caller omitted
|
||||||
|
// reminderEnabled / reminderDays. Honors the /admin/reminders page.
|
||||||
|
const reminderConfig = await getPortReminderConfig(portId);
|
||||||
|
const resolvedReminderEnabled = interestData.reminderEnabled ?? reminderConfig.defaultEnabled;
|
||||||
|
const resolvedReminderDays =
|
||||||
|
interestData.reminderDays ?? (resolvedReminderEnabled ? reminderConfig.defaultDays : null);
|
||||||
|
|
||||||
const result = await withTransaction(async (tx) => {
|
const result = await withTransaction(async (tx) => {
|
||||||
const [interest] = await tx
|
const [interest] = await tx
|
||||||
.insert(interests)
|
.insert(interests)
|
||||||
.values({
|
.values({
|
||||||
portId,
|
portId,
|
||||||
...interestData,
|
...interestData,
|
||||||
|
reminderEnabled: resolvedReminderEnabled,
|
||||||
|
reminderDays: resolvedReminderDays,
|
||||||
leadCategory: resolvedLeadCategory,
|
leadCategory: resolvedLeadCategory,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -237,6 +237,25 @@ export async function listDocumensoWebhookSecrets(): Promise<DocumensoSecretEntr
|
|||||||
|
|
||||||
// ─── Branding ───────────────────────────────────────────────────────────────
|
// ─── Branding ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOT YET WIRED end-to-end. The `/admin/branding` page persists these
|
||||||
|
* settings to system_settings, but the email templates in
|
||||||
|
* `src/lib/email/templates/` and the `<BrandedAuthShell>` component in
|
||||||
|
* `src/components/shared/branded-auth-shell.tsx` still hardcode the
|
||||||
|
* `s3.portnimara.com` logo URL and the Port Nimara color palette. A
|
||||||
|
* second port wired into this CRM will see Port Nimara branding in
|
||||||
|
* every transactional email until those consumers call
|
||||||
|
* `getPortBrandingConfig(portId)`. Tracked as audit finding R2-H15.
|
||||||
|
*
|
||||||
|
* To wire fully:
|
||||||
|
* 1. Take `branding` config as a server-side prop into
|
||||||
|
* `<BrandedAuthShell>` (pass it from the page server component).
|
||||||
|
* 2. Refactor the email shell helper in each `templates/*.ts` module
|
||||||
|
* to take `headerHtml` / `footerHtml` / `primaryColor` instead of
|
||||||
|
* the inline constants.
|
||||||
|
* 3. In each sender, call `getPortBrandingConfig(portId)` and thread
|
||||||
|
* the branding values into the template call.
|
||||||
|
*/
|
||||||
export interface PortBrandingConfig {
|
export interface PortBrandingConfig {
|
||||||
logoUrl: string | null;
|
logoUrl: string | null;
|
||||||
primaryColor: string;
|
primaryColor: string;
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ export const createInterestSchema = z.object({
|
|||||||
source: z.string().optional(),
|
source: z.string().optional(),
|
||||||
notes: z.string().optional(),
|
notes: z.string().optional(),
|
||||||
tagIds: z.array(z.string()).optional().default([]),
|
tagIds: z.array(z.string()).optional().default([]),
|
||||||
reminderEnabled: z.boolean().optional().default(false),
|
// Omitting reminderEnabled / reminderDays falls back to the per-port
|
||||||
|
// defaults configured at /admin/reminders (resolved in
|
||||||
|
// createInterest). To opt out explicitly pass false / null.
|
||||||
|
reminderEnabled: z.boolean().optional(),
|
||||||
reminderDays: z.number().int().min(1).optional(),
|
reminderDays: z.number().int().min(1).optional(),
|
||||||
desiredLengthFt: optionalDesiredDimSchema,
|
desiredLengthFt: optionalDesiredDimSchema,
|
||||||
desiredWidthFt: optionalDesiredDimSchema,
|
desiredWidthFt: optionalDesiredDimSchema,
|
||||||
|
|||||||
Reference in New Issue
Block a user