202 lines
9.0 KiB
Markdown
202 lines
9.0 KiB
Markdown
|
|
# 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).
|