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) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 12:21:45 -04:00
parent 8df8ded46c
commit a8b93fd862

View File

@@ -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).