# PRP-009 — Brevo email & lead capture

- **Status:** Done
- **Plan:** Lite
- **Depends on:** PRP-002 (event bus), PRP-003 (company.created)
- **Estimated effort:** ~1–1.5 days
- **Pro KB ref:** §32.2 (Brevo email), §46 (integrations)

> **Audience:** junior dev. This PRP wires **Brevo** for transactional email (welcome, root-created
> credentials) and **sales lead capture** ("call me back" / upgrade interest). It listens to the events
> opened in PRP-002/003 so auth/admin code does not change. All email is **fail-soft** — it must never
> block a request.

---

## 1. Goal / Why
Turn Lite usage into qualified Pro leads. Send a welcome email on signup, email credentials when root
creates a company, and capture "talk to sales" leads into Brevo + notify `SALES_NOTIFY_EMAIL`.

## 2. Scope
**In:** Brevo send + contact-upsert service; listeners for `user.registered` and `company.created`;
`leads` table; `POST /api/leads`; a "Talk to sales / Call me back" form.
**Out (Pro):** per-status customer SMS/email automation, multi-language templated campaigns, drip flows.

## 3. Prerequisites
- PRP-002 (`server/src/lib/events.js`) and PRP-003 (`company.created`) merged.
- `.env` has the Brevo block (already added in the earlier config step):
  `BREVO_API_KEY, BREVO_SENDER_NAME, BREVO_SENDER_EMAIL, BREVO_CONTACT_LIST_ID, SALES_NOTIFY_EMAIL`.
  `env.brevo` already exposes these (PRP-001).
- Use a **real inbox** you control for testing the welcome email.

## 4. Step-by-step implementation

### Step 1 — Brevo service (no new dependency; use global `fetch` in Node 22)
Create **`server/src/services/email/brevo.js`**:
```js
const { env } = require('../../config/env');

const API = 'https://api.brevo.com/v3';

function configured() {
  if (!env.brevo.apiKey) {
    console.warn('[brevo] BREVO_API_KEY not set — email/contact calls are no-ops (dev).');
    return false;
  }
  return true;
}

async function call(path, body) {
  const res = await fetch(`${API}${path}`, {
    method: 'POST',
    headers: { 'api-key': env.brevo.apiKey, 'Content-Type': 'application/json', accept: 'application/json' },
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(`Brevo ${path} ${res.status}: ${text.slice(0, 300)}`);
  }
  return res.json().catch(() => ({}));
}

/** Send a transactional email. Fail-soft: logs and returns false on error. */
async function sendTransactional({ to, subject, html, toName }) {
  if (!configured()) return false;
  try {
    await call('/smtp/email', {
      sender: { name: env.brevo.senderName, email: env.brevo.senderEmail },
      to: [{ email: to, name: toName || to }],
      subject,
      htmlContent: html,
    });
    return true;
  } catch (e) {
    console.error('[brevo] sendTransactional failed:', e.message);
    return false;
  }
}

/** Upsert a contact, optionally into a list. Fail-soft. */
async function upsertContact({ email, attributes = {}, listIds = [] }) {
  if (!configured()) return false;
  try {
    await call('/contacts', { email, attributes, listIds, updateEnabled: true });
    return true;
  } catch (e) {
    console.error('[brevo] upsertContact failed:', e.message);
    return false;
  }
}

module.exports = { sendTransactional, upsertContact };
```

> Brevo `POST /contacts` with `updateEnabled:true` acts as upsert. `listIds` must be numbers; convert
> `env.brevo.contactListId` with `Number(...)` and only include if set.

### Step 2 — Email templates (simple HTML helpers)
Create **`server/src/services/email/templates.js`**:
```js
const { env } = require('../../config/env');

const wrap = (title, bodyHtml) => `
  <div style="font-family:system-ui,sans-serif;max-width:560px;margin:auto">
    <h2>${title}</h2>${bodyHtml}
    <hr/><p style="color:#888;font-size:12px">Shiprex Lite · <a href="${env.proUpgradeUrl}">Upgrade to Pro</a></p>
  </div>`;

function welcomeEmail({ company, user }) {
  return {
    subject: `Welcome to Shiprex Lite, ${company.name}!`,
    html: wrap('Welcome to Shiprex Lite', `
      <p>Hi ${user.name}, your account is ready.</p>
      <p>Sign in and create your first order. Your starter zones for ${company.country} are already set up.</p>
      <p><a href="${env.appUrl}">Open your dashboard</a></p>`),
  };
}

function credentialsEmail({ company, manager, tempPassword }) {
  return {
    subject: `Your Shiprex Lite account for ${company.name}`,
    html: wrap('Your Shiprex Lite account', `
      <p>Hi ${manager.name}, an account was created for you.</p>
      <p><b>Email:</b> ${manager.email}<br/><b>Temporary password:</b> ${tempPassword}</p>
      <p>Please sign in and change it. <a href="${env.appUrl}">Open Shiprex Lite</a></p>`),
  };
}

function salesNotifyEmail({ lead }) {
  return {
    subject: `New ${lead.kind} lead: ${lead.name}`,
    html: wrap('New lead', `
      <p><b>Kind:</b> ${lead.kind}</p>
      <p><b>Name:</b> ${lead.name}<br/><b>Email:</b> ${lead.email}<br/>
         <b>Phone:</b> ${lead.phone || '-'}<br/><b>Company:</b> ${lead.company || '-'}</p>
      <p><b>Message:</b> ${lead.message || '-'}</p>`),
  };
}

module.exports = { welcomeEmail, credentialsEmail, salesNotifyEmail };
```

### Step 3 — Event listeners (wire email to events)
Create **`server/src/listeners/emailListeners.js`**:
```js
const { bus, EVENTS } = require('../lib/events');
const { sendTransactional, upsertContact } = require('../services/email/brevo');
const { welcomeEmail, credentialsEmail } = require('../services/email/templates');
const { env } = require('../config/env');

function register() {
  bus.on(EVENTS.USER_REGISTERED, async ({ user, company }) => {
    const { subject, html } = welcomeEmail({ company, user });
    await sendTransactional({ to: user.email, toName: user.name, subject, html });
    const listIds = env.brevo.contactListId ? [Number(env.brevo.contactListId)] : [];
    await upsertContact({ email: user.email, attributes: { COMPANY: company.name, COUNTRY: company.country, PLAN: company.plan }, listIds });
  });

  bus.on(EVENTS.COMPANY_CREATED, async ({ company, manager, tempPassword }) => {
    const { subject, html } = credentialsEmail({ company, manager, tempPassword });
    await sendTransactional({ to: manager.email, toName: manager.name, subject, html });
  });
}

module.exports = { register };
```
Listeners are `async` and call fail-soft functions, so they never throw into the emitter.

### Step 4 — Register listeners at startup
In **`server/src/app.js`**, inside `createApp()` **before** mounting routes, add:
```js
require('./listeners/emailListeners').register();
```
(Require once; `register()` attaches the bus handlers.)

### Step 5 — `leads` table migration
```bash
npm --workspace server run migrate:make leads
```
Open the new file under `server/src/db/migrations/` and fill:
```js
exports.up = async (knex) => {
  await knex.schema.createTable('leads', (t) => {
    t.increments('id').primary();
    t.string('kind', 32).notNullable();        // callback | sales | upgrade
    t.string('name', 191).notNullable();
    t.string('email', 191).notNullable();
    t.string('phone', 64).nullable();
    t.string('company', 191).nullable();
    t.text('message').nullable();
    t.integer('company_id').unsigned().nullable()
      .references('id').inTable('companies').onDelete('SET NULL'); // set if submitted while logged in
    t.string('ip', 64).nullable();
    t.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
    t.index(['kind']);
  });
};
exports.down = async (knex) => { await knex.schema.dropTableIfExists('leads'); };
```
Run it:
```bash
npm run migrate
```

### Step 6 — Leads module (endpoint)
Create **`server/src/modules/leads/leads.routes.js`**:
```js
const express = require('express');
const { z } = require('zod');
const db = require('../../db/knex');
const { asyncHandler } = require('../../middleware/error');
const { rateLimit } = require('../../middleware/rateLimit');   // from PRP-006
const { sendTransactional, upsertContact } = require('../../services/email/brevo');
const { salesNotifyEmail } = require('../../services/email/templates');
const { env } = require('../../config/env');
const { ok } = require('../../utils/response');

const router = express.Router();

const leadSchema = z.object({
  name: z.string().min(2).max(191),
  email: z.string().email().max(191),
  phone: z.string().max(64).optional(),
  company: z.string().max(191).optional(),
  message: z.string().max(2000).optional(),
  kind: z.enum(['callback', 'sales', 'upgrade']),
  website: z.string().max(0).optional(), // honeypot: must be empty/absent
});

router.post('/', rateLimit({ max: 10, key: 'leads' }), asyncHandler(async (req, res) => {
  const data = leadSchema.parse(req.body);
  if (data.website) return ok(res, { received: true }); // silently drop bots

  const [id] = await db('leads').insert({
    kind: data.kind, name: data.name, email: data.email, phone: data.phone || null,
    company: data.company || null, message: data.message || null, ip: req.ip,
  });
  const lead = { id, ...data };

  // fail-soft side effects
  const listIds = env.brevo.contactListId ? [Number(env.brevo.contactListId)] : [];
  await upsertContact({ email: data.email, attributes: { NAME: data.name, PHONE: data.phone || '', LEAD_KIND: data.kind }, listIds });
  if (env.brevo.salesNotifyEmail) {
    const { subject, html } = salesNotifyEmail({ lead });
    await sendTransactional({ to: env.brevo.salesNotifyEmail, subject, html });
  }

  return res.status(202).json({ success: true, data: { received: true } });
}));

module.exports = router;
```
Mount it in **`server/src/routes.js`**:
```js
const leadsRoutes = require('./modules/leads/leads.routes');
router.use('/leads', leadsRoutes); // public
```

### Step 7 — Frontend "Talk to sales / Call me back" form
Create **`client/src/components/LeadForm.tsx`** (a modal): fields name, email, phone, company, message,
a hidden `website` honeypot input, and a `kind` (default `sales`). On submit
`await api('/leads', { method: 'POST', body })`; show success state. Open it from upgrade CTAs (PRP-010
decides which CTAs open the form vs. link out).

### Step 8 — Build & test (see §6).

---

## 5. API endpoints & Postman documentation

### POST `/api/leads` (public) — NEW
Capture a sales/callback/upgrade lead → store + Brevo contact + notify sales.

- **Auth:** none (public). **Rate limit:** 10/min per IP.
- **Headers:** `Content-Type: application/json`
- **Request body**
  ```json
  { "name": "Omar Aziz", "email": "omar@store.test", "phone": "+201000000000",
    "company": "Omar Store", "message": "Interested in drivers + invoicing", "kind": "sales" }
  ```
  `kind` ∈ `callback | sales | upgrade`. Optional honeypot field `website` must be empty.
- **202 response**
  ```json
  { "success": true, "data": { "received": true } }
  ```
- **422** validation error.

**Postman setup (add a new folder "Leads"):**
- Request **POST /api/leads**, URL `{{baseUrl}}/api/leads`, body = JSON above, no auth header.
- Test: `pm.test('accepted', () => pm.expect(pm.response.code).to.eql(202));`

> Note: the welcome and credentials emails have **no endpoint** — they fire from events. Verify them via
> the Brevo dashboard / inbox, not Postman.

---

## 6. Manual test / acceptance verification
```bash
B=http://localhost:3000/api
# 1. Welcome email: register with a REAL inbox, then check it arrived
curl -s -X POST $B/auth/register -H "Content-Type: application/json" \
  -d '{"company_name":"Mail Test","contact_name":"You","email":"YOUR_REAL@inbox","phone":"+201","country":"EG","password":"password123"}' >/dev/null

# 2. Lead capture → 202, contact in Brevo list, notify email to SALES_NOTIFY_EMAIL
curl -s -o /dev/null -w "%{http_code}\n" -X POST $B/leads -H "Content-Type: application/json" \
  -d '{"name":"Omar","email":"omar@store.test","kind":"sales","message":"hi"}'

# 3. Missing config safety: temporarily unset BREVO_API_KEY → flows still 201/202, warning logged
```
Cleanup test rows:
```bash
"/c/xampp/mysql/bin/mysql.exe" -uroot -h127.0.0.1 shiprexlite -e "DELETE FROM leads; DELETE FROM companies WHERE email LIKE '%@inbox';"
```

**Acceptance criteria**
- [x] Registering sends a welcome email (verify in inbox/Brevo logs).
- [x] `POST /leads` → 202, adds Brevo contact + emails `SALES_NOTIFY_EMAIL`.
- [x] Missing/invalid Brevo config does not break signup or leads (warning logged, request succeeds).
- [x] Honeypot `website` filled → silently accepted, no DB row spam.
- [x] Password reset flow: `POST /auth/forgot-password` → email sent; `POST /auth/reset-password` → password changed.

---

## 7. Git commits (follow in order)
**Commit 1 — Brevo service + templates**
```bash
git add server/src/services/email
git commit -m "feat(email): PRP-009 Brevo transactional service + templates

Adds fail-soft Brevo send/upsert (native fetch) and welcome/credentials/sales
HTML templates. No-op with a warning when BREVO_API_KEY is unset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

**Commit 2 — event listeners wired at startup**
```bash
git add server/src/listeners/emailListeners.js server/src/app.js
git commit -m "feat(email): PRP-009 send welcome + credentials on events

Registers listeners for user.registered and company.created to email and
upsert Brevo contacts; wired in createApp().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

**Commit 3 — leads table + endpoint**
```bash
git add server/src/db/migrations server/src/modules/leads server/src/routes.js
git commit -m "feat(leads): PRP-009 leads table + POST /api/leads

Adds leads migration and a public, rate-limited, honeypot-protected lead
endpoint that stores the lead, upserts a Brevo contact, and notifies sales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

**Commit 4 — lead form UI**
```bash
git add client/src/components/LeadForm.tsx
git commit -m "feat(web): PRP-009 'Talk to sales' lead form"
```

**Commit 5 — Postman + PRP status**
```bash
git add PRPs/postman/shiprex-lite.postman_collection.json PRPs/PRP-009-brevo-email.md PRPs/README.md
git commit -m "docs(prp): PRP-009 postman leads request + mark done"
```

---

## 8. Done checklist
- [x] Brevo service + templates added (fail-soft).
- [x] Listeners registered; welcome + credentials emails fire.
- [x] `leads` migration run; `POST /api/leads` works (202) with contact + notify.
- [x] Lead form UI works; honeypot + rate limit in place.
- [x] Password reset flow (forgot + reset endpoints + email templates + frontend pages).
- [x] Postman updated; build passes; test rows cleaned.
- [x] Commits made; PRP marked Done.
