Email-only contacts
Prerequisites
No test cases yet.
Pre-deploy / migration
| ID | Test Case | Preconditions | Steps | Expected Result | Priority |
|---|---|---|---|---|---|
| EOC-001 | Migration applies cleanly | DB backup taken, app stopped or blue/green ready | 1. Run `alembic upgrade head` 2. Verify revision `0086_contact_email_id` | Migration succeeds. No constraint violations. Existing contacts with `phone=''` now have `phone IS NULL` | High |
| EOC-002 | Email-only row allowed in DB | Migration applied | 1. `INSERT` or API-create contact with `phone=NULL`, `email='verify@example.com'` (unique per org) | Row persists. Check constraints satisfied (`phone OR email` required) | High |
| EOC-003 | Partial unique indexes | Two contacts in same org | 1. Create contact A: phone only 2. Create contact B: email only 3. Attempt duplicate phone or duplicate email in same org | Second create returns 409 / unique violation. Different orgs may reuse same email (tenant-scoped) | High |
CRM — create & edit contacts
| ID | Test Case | Preconditions | Steps | Expected Result | Priority |
|---|---|---|---|---|---|
| EOC-010 | Create email-only contact via UI | Logged in, `/contacts` | 1. Click **New Contact** 2. Leave phone empty 3. Enter email `alice@example.com`, name optional 4. Save | Success toast. List shows email; phone column shows **—**. No placeholder phone stored | High |
| EOC-011 | Create phone-only contact (regression) | Logged in | 1. New contact with phone `+919876543210`, email empty 2. Save | Contact created as before. `phone` set, `email` null | High |
| EOC-012 | Create with both phone and email | Logged in | 1. Enter phone and email 2. Save | Both fields stored. Open Chat enabled | High |
| EOC-013 | Validation: neither phone nor email | Create modal open | 1. Leave phone and email empty 2. Save | Inline/form error: at least one of phone or email required. No API call | High |
| EOC-014 | Validation: invalid email | Create modal open | 1. Email only: `not-an-email` 2. Save | Client or API validation error. Contact not created | Medium |
| EOC-015 | Search by email | Email-only contact exists | 1. `/contacts` search box: partial email 2. Observe results | Contact appears in results (email search works) | High |
| EOC-016 | Open Chat disabled for email-only | Email-only contact in list | 1. Hover Open Chat icon 2. Click (if enabled) | Button disabled or tooltip: add phone to start WhatsApp chat. No navigation to conversation | High |
| EOC-017 | Backfill phone on email-only contact | Email-only contact exists | 1. Edit contact 2. Add phone (field enabled when contact has no phone) 3. Save | Phone saved. Open Chat becomes enabled. Phone field read-only on subsequent edits | High |
| EOC-018 | Cannot change phone once set | Contact with phone | 1. Edit contact 2. Attempt to change phone | Phone field disabled. Only email/name/tags editable | High |
| EOC-019 | Duplicate email (API) | Contact with `bob@example.com` exists | 1. `POST /api/contacts` with same email, different phone (or curl) | HTTP **409** with message containing `already exists` | High |
| EOC-020 | Identity collision (API) | Contact A: phone P1. Contact B: email E2 only | 1. `POST /api/contacts` with `phone=P1` and `email=E2` | HTTP **409** `identity_collision: phone and email match different contacts` | High |
Public forms & marketing
| ID | Test Case | Preconditions | Steps | Expected Result | Priority |
|---|---|---|---|---|---|
| EOC-030 | Marketing `/contact_us` email-only | Public site or local `frontend` dev | 1. Open `/contact_us` 2. Fill name + email only (no phone) 3. Submit | Success message mentions email follow-up (not only WhatsApp). Network payload has `"phone": null` | High |
| EOC-031 | Marketing form phone + email | Public form | 1. Submit with both phone and email | Success copy mentions WhatsApp confirmation. Payload includes phone string | Medium |
| EOC-032 | Marketing validation: neither | Form open | 1. Fill name only, submit | Client validation highlights phone and email fields. No submit | High |
| EOC-033 | Public API email-only lead | Valid `X-Form-Token`, honeypot empty | 1. `POST /api/public/forms/submit` JSON: `{ "name": "Ravi", "email": "ravi@example.com" }` | HTTP **200** `{"ok": true}`. Contact + lead created; contact has `phone=NULL` | High |
| EOC-034 | Public API missing both identifiers | Valid token | 1. POST body: `{ "name": "Ravi" }` only | HTTP **422** validation error | High |
| EOC-035 | Form integration guide endpoint | Open `/feature-coverage-matrix` or form guide page source | 1. Inspect documented submit URL | Endpoint is `https://api.bizbotify.com/api/public/forms/submit` (not `app.bizbotify.com`) | Low |
Leads panel
| ID | Test Case | Preconditions | Steps | Expected Result | Priority |
|---|---|---|---|---|---|
| EOC-040 | Lead from email-only form | Lead created from EOC-033 | 1. Open Leads 2. Find lead 3. Open detail panel | Lead opens. **Open Chat** disabled. Hint: no WhatsApp number on file | High |
| EOC-041 | Lead Open Chat after phone backfill | Email-only lead/contact | 1. Add phone on linked contact (EOC-017) 2. Reopen lead panel | Open Chat enabled. Chat opens or creates conversation | Medium |
WhatsApp campaigns
| ID | Test Case | Preconditions | Steps | Expected Result | Priority |
|---|---|---|---|---|---|
| EOC-050 | Broadcast excludes email-only | Email-only + phone contact in org; template approved | 1. Create broadcast, recipient source **Contacts**, select both 2. Launch campaign | `total_recipients` counts only phone contact. Email-only excluded (not FAILED row) | High |
| EOC-051 | Campaign completes with mixed audience | As above | 1. Run campaign to completion | Campaign status completed. Only phone contact receives message. Logs contain `campaign_skipped_undeliverable` with `channel=whatsapp` for skipped contact | High |
| EOC-052 | Send test template to phone | Active WA account | 1. Campaign wizard → send test to valid phone | Test message delivers | Medium |
Auto follow-up / nurture
| ID | Test Case | Preconditions | Steps | Expected Result | Priority |
|---|---|---|---|---|---|
| EOC-060 | Manual enroll skipped (email-only) | Active nurture sequence; email-only contact | 1. Contacts → contact → enroll in follow-up OR Leads enroll | HTTP **400** or UI error: no phone for WhatsApp follow-up. No enrollment row | High |
| EOC-061 | Auto-enroll after form (email-only) | Sequence with tag/form trigger | 1. Submit public form email-only with matching tag 2. Check logs | `nurture_enroll_skipped_undeliverable` logged; no active enrollment | Medium |
| EOC-062 | Nurture on phone contact (regression) | Contact with phone | 1. Enroll in sequence | Enrollment created. Steps send on schedule | High |
Agent inbox / outbound messages
| ID | Test Case | Preconditions | Steps | Expected Result | Priority |
|---|---|---|---|---|---|
| EOC-070 | Send text without phone (API) | Conversation linked to contact; set `contact.phone=NULL` in DB **or** use API-only path | 1. `POST /api/conversations/{id}/messages/text` with body | HTTP **400** detail contains no phone number. No Meta API call | High |
| EOC-071 | Inbound WhatsApp still works (regression) | Customer messages business number | 1. Send WhatsApp to connected number 2. Verify inbox | Message appears. Contact has phone from webhook. Auto-reply/flows still run | High |
Chatbot / automation (flow engine)
| ID | Test Case | Preconditions | Steps | Expected Result | Priority |
|---|---|---|---|---|---|
| EOC-080 | Flow send_message skip | Email-only contact in flow session (edge setup) | 1. Trigger flow with send_message node | Step skipped; log `messaging_skipped_undeliverable` surface `flow_engine_send_message`. Flow does not crash | Medium |
| EOC-081 | Flow send_template skip | Same | 1. Flow with send_template node | No template sent; flow advances or skips gracefully | Medium |
API & integrations contract
| ID | Test Case | Preconditions | Steps | Expected Result | Priority |
|---|---|---|---|---|---|
| EOC-090 | ContactResponse phone nullable | Email-only contact | 1. `GET /api/contacts/{id}` | JSON `"phone": null`. Email present | High |
| EOC-091 | Integrations contact detail | API key with contact read | 1. `GET /api/integrations/v1/contacts/{id}` for email-only | `phone` omitted or null per schema | Medium |
| EOC-092 | CSV export email-only row | Email-only contact exists | 1. Export contacts CSV | Row has empty phone cell, email populated | Medium |
Observability (logs)
| ID | Test Case | Preconditions | Steps | Expected Result | Priority |
|---|---|---|---|---|---|
| EOC-100 | Email-only create log | Create via API/form | 1. Create email-only contact 2. Search logs `contact_created_email_only` | Log line with `org_id`, `contact_id` (no PII) | Medium |
| EOC-101 | Campaign skip log | EOC-050 | 1. Build campaign recipients 2. Search `campaign_skipped_undeliverable` | `channel=whatsapp`, `contact_id`, `campaign_id` | Medium |
| EOC-102 | Form collision log | Two contacts; submit form with conflicting phone+email | 1. Submit public form matching collision scenario 2. Search `identity_collision` | Warning logged with both contact IDs; form still returns 200 | Medium |
Explicitly out of scope (do not file bugs)
No test cases yet.
Sign-off checklist
No test cases yet.