# Inquiry Notifications System Design Migrates the ActivePieces-powered inquiry notification flow into the CRM. When a client registers interest via the Port Nimara website, the system sends a confirmation email to the client and notifies the sales team -- all using the CRM's own database and email infrastructure instead of NocoDB + ActivePieces. ## Scope - Expand the public interest API to accept all website form fields - Add client address storage (multi-address with primary flag) - Send branded confirmation email to the client - Send notification to sales team (CRM users + optional external recipients) - Make notification recipients and contact email configurable by admins ## Database Changes ### New table: `client_addresses` | Column | Type | Notes | | ---------------- | ----------------- | ---------------------------------------------------------------- | | `id` | uuid PK | `crypto.randomUUID()` | | `client_id` | uuid FK → clients | cascade delete | | `port_id` | uuid FK → ports | cascade delete | | `label` | text | e.g., "Home", "Office", "Billing" | | `street_address` | text | | | `city` | text | | | `state_province` | text | | | `postal_code` | text | | | `country` | text | | | `is_primary` | boolean | default `true`, one-primary-per-client enforced in service layer | | `created_at` | timestamp | default `now()` | | `updated_at` | timestamp | default `now()` | Schema file: `src/lib/db/schema/clients.ts` (alongside existing client tables). Relations: added to `src/lib/db/schema/relations.ts` (client has many addresses). ### No changes to existing tables - `clients.preferred_contact_method` already exists -- we populate it from the form. - `interests.berth_id` already exists -- we resolve `mooringNumber` to a berth and link it. - `notifications.type` already has `new_registration` -- we fire it. ## Public API Changes ### `POST /api/public/interests` Expanded request schema: ```typescript // Required firstName: string; // max 100 lastName: string; // max 100 email: string; // email format phone: string; // Optional preferredContactMethod: 'email' | 'phone' | 'sms'; mooringNumber: string; // e.g., "A3" -- resolved against berths.mooring_number companyName: string; yachtName: string; yachtLengthFt: number; yachtWidthFt: number; yachtDraftFt: number; preferredBerthSize: string; notes: string; // max 2000 address: { street: string; city: string; stateProvince: string; postalCode: string; country: string; } // Backward compatibility fullName: string; // accepted if firstName/lastName not provided ``` Backward compatibility: if `fullName` is provided without `firstName`/`lastName`, it is used as-is for `clients.full_name`. If `firstName`+`lastName` are provided, they are concatenated. ### Behavior after record creation 1. Resolve `mooringNumber` against `berths.mooring_number` for the port. Link `interests.berth_id` if found; leave null if not. 2. Store `address` in `client_addresses` with `is_primary: true` and `label: 'Primary'`. 3. Set `clients.preferred_contact_method` from the form value. 4. Queue client confirmation email (see Email Templates below). 5. Fire `new_registration` notifications to sales team (see Notification Flow below). 6. Return `201 { data: { id, message } }` unchanged. Rate limiting remains 5 requests/hour per IP. ## Email Templates Located in `src/lib/email/templates/`. Each exports a function that accepts a typed data object and returns `{ subject: string, html: string, text: string }`. ### `inquiry-client-confirmation.ts` Sent to the client who submitted the form. **Input data:** - `firstName` -- for the greeting - `mooringNumber` -- berth identifier (nullable) - `contactEmail` -- from `inquiry_contact_email` system setting **Subject:** "Thank You for Your Interest in Berth {mooringNumber}" or "Thank You for Your Interest in a Port Nimara Berth" if no berth. **Body:** Greeting with first name, confirmation their interest is registered, mention they'll be contacted by preferred method, link to the contact email address. **Styling:** Branded -- Port Nimara logo, background image, white card layout. Matches the existing ActivePieces client confirmation template. ### `inquiry-sales-notification.ts` Sent to CRM users and optional external recipients. **Input data:** - `fullName` - `email` - `phone` - `mooringNumber` (nullable, defaults to "None") - `crmUrl` -- link to the interest detail page in the CRM (built from port slug + interest ID) **Subject:** "New Interest - Port Nimara" **Body:** Notifies that a new interest has been registered, shows client details and berth selected, links to the CRM. **Styling:** Branded -- Port Nimara logo, background image, white card layout. Matches the existing ActivePieces admin notification template. Both templates include a plain-text fallback. ## Notification & Delivery Flow ### Client confirmation email 1. After record creation, queue a `send-inquiry-confirmation` job on the `email` BullMQ queue. 2. Email worker renders the `inquiry-client-confirmation` template with the interest data. 3. Sends via system SMTP (`src/lib/email/index.ts`). 4. No in-app notification (client is not a CRM user). ### Sales team notification 1. Query all users on the port who have `interests` read permission via their role. 2. For each user, call `createNotification()` with type `new_registration`. - The existing notification service checks `user_notification_preferences` (in-app / email / both / neither). - Creates in-app notification + Socket.IO push if `in_app: true`. - Queues `send-notification-email` job if `email: true`. 3. Fetch `inquiry_notification_recipients` system setting for the port. 4. For each external email, queue a `send-inquiry-sales-notification` job on the `email` queue (bypasses notification preferences since these are not CRM users). ### Independence Client confirmation and sales notifications are independent -- a failure in one does not block the other. The `201` response returns immediately after record creation, before any emails are sent. ## Admin Configuration Two new system settings, managed via the existing admin settings UI: ### `inquiry_contact_email` (string, per-port) The reply-to / contact email shown in client confirmation emails. - Default: `sales@portnimara.com` - Displayed as a mailto link in the client confirmation email. ### `inquiry_notification_recipients` (JSON array of strings, per-port) Additional external email addresses that receive the sales team notification. - Default: `[]` (empty) - Only CRM users with interests permissions are notified by default. - External recipients receive the sales notification email directly. ### Existing infrastructure (no changes needed) - **Which CRM users get notified**: controlled by roles/permissions. - **How each user receives notifications**: `user_notification_preferences` table. - **Admin settings UI**: already supports custom key-value pairs in `system_settings`. ## Files to Create or Modify ### New files - `src/lib/db/schema/client-addresses.ts` -- (or added to `clients.ts`) - `src/lib/email/templates/inquiry-client-confirmation.ts` - `src/lib/email/templates/inquiry-sales-notification.ts` ### Modified files - `src/lib/db/schema/clients.ts` -- add `clientAddresses` table export - `src/lib/db/schema/index.ts` -- re-export new table - `src/lib/db/schema/relations.ts` -- add client addresses relations - `src/lib/validators/public-interest.ts` (or wherever `publicInterestSchema` lives) -- expand schema - `src/app/api/public/interests/route.ts` -- berth resolution, address storage, notification + email triggers - `src/lib/queue/workers/email.ts` -- handle `send-inquiry-confirmation` and `send-inquiry-sales-notification` jobs - `src/lib/services/interests.service.ts` -- helper to find users with interests permissions on a port - `src/app/(dashboard)/[portSlug]/admin/settings/settings-manager.tsx` -- register the two new setting keys ## Out of Scope - Editing email templates from the admin UI (templates are in code). - Supplemental forms for collecting missing info (separate feature using existing `form_templates` / `form_submissions` infrastructure). - Documenso EOI integration with address merge fields (separate feature). - Changes to the Port Nimara website form itself (website team wires the form to our API).