From a8b93fd86212c875748b3654949ff30148f161ed Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 14 Apr 2026 12:21:45 -0400 Subject: [PATCH] Add inquiry notifications system design spec Covers migrating ActivePieces inquiry flow into the CRM: client confirmation emails, sales team notifications, client addresses table, and admin configuration. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...2026-04-14-inquiry-notifications-design.md | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-14-inquiry-notifications-design.md diff --git a/docs/superpowers/specs/2026-04-14-inquiry-notifications-design.md b/docs/superpowers/specs/2026-04-14-inquiry-notifications-design.md new file mode 100644 index 0000000..f024011 --- /dev/null +++ b/docs/superpowers/specs/2026-04-14-inquiry-notifications-design.md @@ -0,0 +1,201 @@ +# 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).