Files
pn-new-crm/docs/superpowers/specs/2026-04-14-inquiry-notifications-design.md
Matt a8b93fd862 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>
2026-04-14 12:21:45 -04:00

9.0 KiB

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:

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