═══════════════════════════════════════════════════════════════════════════════ MANUAL TESTING GUIDE — Customer Management mini-PWA URL: https://srv1111289.hstgr.cloud/sales/customers/ SW version: customers-v4 Default access: harish, pramod ═══════════════════════════════════════════════════════════════════════════════ This guide covers EVERY screen, form, modal, and DB interaction that the PWA can produce. It is meant for a manual tester (human, not Claude-in-Chrome) to exercise the flow + logic end-to-end with concrete test entries. Sections A. Pre-flight (setup & data load) B. Customer master CRUD (Phase 1) C. Contacts CRUD D. Addresses CRUD E. Cross-PWA picker on Sales (Phase 2) F. Lead lifecycle + conversion (Phase 3) G. Opportunity lifecycle (Phase 3) H. Communications + sales-person picker (Phase 4) I. Managed lookups + 360 + By Owner (Phase 5) J. Access gating K. Negative tests L. Tear-down ╔═════════════════════════════════════════════════════════════════════════════╗ ║ A. PRE-FLIGHT ║ ╚═════════════════════════════════════════════════════════════════════════════╝ A1. Confirm migrations are applied. SSH into VPS, then: docker exec -i postgres psql -U lmadmin -d lm360 -c "\dt sales.*" Expect tables: customers, customer_contacts, customer_addresses, leads, opportunities, pipeline_history, communications, customer_groups, territories, industries (plus pre-existing jobs, offers, etc.) A2. Load test dataset: docker exec -i postgres psql -U lmadmin -d lm360 \ < /var/www/360lm/sales/customers/test_data.sql Final report should print: customers 8 contacts 13 addresses 10 leads 6 opportunities 7 communications 12 A3. Log in via /hub/ as harish (or pramod). Open /sales/customers/. First load: - Three tiles populate within 1 second. - Card list scrolls (8 seed customers + any production rows). - Bottom nav shows: 📋 List · 🔁 Pipeline · ➕ Add. - User pill ("👤 Harish Kumar Lal") visible top-right. A4. View source / DevTools → confirm SW: customers-v4 is registered. ╔═════════════════════════════════════════════════════════════════════════════╗ ║ B. CUSTOMER MASTER CRUD ║ ╚═════════════════════════════════════════════════════════════════════════════╝ B1. CREATE — Business with full GSTIN ➕ Add → switch chip to "Business" (default) → fill: Name: "Test Pvt Ltd" Proprietor: "Mr. Test Owner" Display: "Test Pvt" GSTIN: "07AAACT1234A1Z5" (15 chars; should auto-uppercase) PAN: "AAACT1234A" (10 chars; auto-uppercase) Phone: "9810000000" Email: "test@example.com" Industry: type "Manu" — should NOT match → free-typed Group: pick "Standard" from datalist Territory: pick "Delhi-NCR" Currency: INR Payment: "Net 30" Aliases: "Test, T-Pvt" Notes: "Created by manual test B1" Tap Save. Expected: ✅ Toast "Customer added" ✅ Returns to List ✅ "Test Pvt Ltd" visible in list ✅ After 1–2s, "Manu" appears in Industry datalist on next form open (auto-persisted as new lookup). B2. CREATE — Individual minimal ➕ Add → switch chip to "Individual" → only fill Name = "Test Person". Save. ✅ Toast, list shows new row with badge "Individual". B3. CREATE — Missing name validation ➕ Add → leave Name empty → Save. ❌ Toast "Name is required" (error color). Form stays open. B4. EDIT — change phone & territory Open "Test Pvt Ltd" → ✏️ Edit → change Phone to "9820111111", Territory to free-type "Test Town". Save. ✅ Detail screen shows updated phone in Info card. ✅ Next form open shows "Test Town" in Territory datalist. B5. SOFT-DELETE Open "Test Person" → Edit → 🗑️ Soft-delete → confirm. ✅ Toast "Customer deactivated". ✅ List no longer shows that row (the list query filters is_active=true). DB check (SSH): docker exec postgres psql -U lmadmin -d lm360 \ -c "SELECT name, is_active FROM sales.customers WHERE name='Test Person';" Expect: is_active = f. B6. RESTORE (admin-only via SQL — no UI yet) UPDATE sales.customers SET is_active=true WHERE name='Test Person'; Reload list — row reappears. ╔═════════════════════════════════════════════════════════════════════════════╗ ║ C. CONTACTS CRUD ║ ╚═════════════════════════════════════════════════════════════════════════════╝ C1. ADD CONTACT Open "Test Pvt Ltd" → Contacts tab → "+ Add contact" → Name: "Ms. Contact One" Role: "billing" Designation: "Accounts Lead" Phone: "9830000001" Email: "c1@test.in" Tick "Mark as primary contact" Save. ✅ Card appears in Contacts list with "primary" + "billing" + "PRIMARY" badges. C2. ADD SECOND CONTACT (decision-maker) Same flow, Name "Mr. Contact Two", Role "decision_maker", do NOT tick primary. Save. ✅ Card appears below the first. C3. EDIT CONTACT Tap "Ms. Contact One" card → modal opens with prefilled data → change Designation to "Senior AC" → Save. ✅ Card updates. C4. PROMOTE NEW PRIMARY (data note) Tap "Mr. Contact Two" → tick "Mark as primary" → Save. ✅ Both contacts may now show "PRIMARY" badge (the schema allows multiple is_primary=true rows; only the view's `is_primary DESC` order matters for primary-contact resolution). C5. DELETE CONTACT Tap "Mr. Contact Two" → "Delete" → confirm. ✅ Card removed. C6. PRIMARY-CONTACT RESOLUTION Hard-reload the page (Ctrl-Shift-R). Find "Test Pvt Ltd" in the list. Expect meta row to show "👤 Ms. Contact One" since she is the remaining primary. ╔═════════════════════════════════════════════════════════════════════════════╗ ║ D. ADDRESSES CRUD ║ ╚═════════════════════════════════════════════════════════════════════════════╝ D1. ADD BILLING (default) Detail → Addresses → "+ Add address": Type: Billing Line 1: "12, Test Plaza" Line 2: "Tower B" City: "Gurugram" State: "Haryana" State code: "06" Pincode: "122001" Tick "Default address" Save. ✅ Card shows "billing", "DEFAULT", "SC: 06", and full address. D2. ADD SHIPPING "+ Add address": Type Shipping, Line 1 "Industrial Park Unit 7", City Manesar, State Haryana, Code 06, Pincode 122050. ✅ Card appears, no DEFAULT badge. D3. EDIT — flip default Tap the shipping card → tick "Default address" → Save. ✅ Both cards may show DEFAULT briefly (schema allows multiple); the view's `is_default DESC` picks the latest. Refresh list — meta row on the customer card shows 📍 Manesar (the new default city). D4. DELETE — billing Tap billing → Delete → confirm. ✅ Removed. ╔═════════════════════════════════════════════════════════════════════════════╗ ║ E. SALES PWA INTEGRATION (PHASE 2) ║ ╚═════════════════════════════════════════════════════════════════════════════╝ E1. JOB-FORM PICKER /sales/ → New Job. Fill year+brand+campaign. In Client input type "verve": ✅ Dropdown shows "Verve Marketing Solutions · BIZ · GST 27AAACV5678G1Z3 · 👤 Anita Sharma · 9820098765 · Mumbai". Click suggestion. Field fills, "✓ linked" pill appears. E2. JOB SAVE WITH LINK Pick at least one zone (required), then Save. DB check: SELECT job_id, client, customer_id FROM sales.jobs WHERE created_at > NOW() - INTERVAL '5 minutes' ORDER BY created_at DESC LIMIT 1; ✅ Row has customer_id = 'cm-test-verve-mktg'. E3. OFFER-FORM AUTO-FILL Open the job you just saved → Offers tab → "+ New Offer". Client field is pre-populated with brand/client. Clear it, type "park": ✅ Suggestion appears for "Park Hospital Pvt Ltd". Click. ✅ Address auto-fills "Plot 27, Sector 15"; State "06 — Haryana"; GSTIN auto-fills; phone/email auto-fill from Park record. Add at least one offer line, Save. ✅ Offer row stored with customer_id = 'cm-test-park-hospital' (verify in DB). E4. INLINE QUICK-ADD New Offer → Client input → type "BrandX Mfg Pvt {your initials}" (a new name). ✅ Last dropdown row is green "➕ Add 'BrandX Mfg Pvt XX' as new customer". Click. ✅ Toast "Customer added & linked". Field carries the link pill. E5. RETURN TO CUSTOMER PWA /sales/customers/. Search "brandx" — see new customer with note "Quick-added from Sales PWA". E6. LEGACY FREE-TEXT PATH (no breakage proof) New Offer → Client → type "Some Random Co Not In DB" → DON'T click any suggestion → submit form anyway. ✅ Saves successfully (client_name = free text; customer_id = NULL). Verify in DB. ╔═════════════════════════════════════════════════════════════════════════════╗ ║ F. LEAD LIFECYCLE ║ ╚═════════════════════════════════════════════════════════════════════════════╝ F1. CREATE LEAD 🔁 Pipeline → "+ Lead": Name "Test Lead Person" Company "TLP Industries" Status: New Source: "Referral" Phone "9700112233" Email "lead@tlp.in" Expected value: 250000 Expected close: today + 14 days Industry: "Hospitality" (pick from datalist) Territory: "Mumbai" (pick) Owner: pick "Harish Kumar Lal" Notes: "Manual test lead F1" Save. ✅ Toast saved. Card appears in "New" column. F2. PROGRESS STATUS — New → Contacted Click the new card → modal → change Status to "Contacted" → Save. ✅ Card moves to "Contacted" column. History line appears in modal on reopen: "new → contacted by harish". F3. PROGRESS — Contacted → Qualified Reopen → Status Qualified → Save. Card moves again. History grows. F4. ACTIVITY — log a call Reopen → Activity section composer: Kind: 📞 Call Direction: ↗ Outbound Owner: defaults to current user Subject "Discovery call" Body "Discussed scope, will share proposal" Follow-up: today + 3 days "Log activity". ✅ Card appears at top of thread. Pipeline header should now show one more pending follow-up. F5. CONVERT TO CUSTOMER "🔁 Convert to Customer" → confirm. ✅ Toast "Converted — customer + opportunity created". ✅ Lead status -> Converted (in modal AND kanban — moves to Converted col). ✅ A new customer "TLP Industries" exists in list (search "tlp"). ✅ "Test Lead Person" becomes a primary contact on TLP Industries (Contacts tab). ✅ Aroma's expected_value 250000 → new opp in Qualification stage, owner harish, name based on lead notes. F6. CONVERSION IDEMPOTENCY Reopen the (now-converted) lead → Convert button is GONE. F7. SOFT-DELETE LEAD Open any other test lead → 🗑️ Soft-delete → confirm. ✅ Lead disappears from kanban (is_active=false). DB row remains. ╔═════════════════════════════════════════════════════════════════════════════╗ ║ G. OPPORTUNITY LIFECYCLE ║ ╚═════════════════════════════════════════════════════════════════════════════╝ G1. CREATE OPP MANUALLY Pipeline → "+ Opp": Customer: pick "Asian Paints Limited" Deal name: "Q3 Promo Activation" Stage: Proposal Owner: harish Expected value: 1500000 Expected close: today + 30 Description: "Manual test G1" Save. ✅ Appears in Proposal column on Opps kanban. G2. PROGRESS STAGE — Proposal → Negotiation Click card → Stage = Negotiation → Save. ✅ Moves columns. G3. WIN Stage = Won → Save. ✅ Moves to Won column. DB check: SELECT won_at FROM sales.opportunities WHERE opp_id = ''; ✅ won_at is set (auto-stamped by trigger). G4. LOSE A SEPARATE OPP Create another opp on a different customer. Stage = Lost. A "Loss reason" field appears — fill "Test lost reason". Save. ✅ Card appears in Lost column. G5. HISTORY Reopen the won opp → History section shows "proposal → negotiation" and "negotiation → won" entries by harish. G6. OWNER FILTER Click "Harish Kumar Lal" owner chip — kanban filters. Open Pipeline → Opportunities (still on harish filter) → By Owner table still shows totals for ALL owners (the table is unfiltered by design). G7. SOFT-DELETE OPP Open the lost test opp → Delete → confirm. ✅ Disappears. ╔═════════════════════════════════════════════════════════════════════════════╗ ║ H. COMMUNICATIONS + OWNER PICKER ║ ╚═════════════════════════════════════════════════════════════════════════════╝ H1. CUSTOMER-LEVEL ACTIVITY Open Park Hospital → Activity tab → seed shows 2 cards already. Add 1 note via composer (kind Note, body "Manual test H1"). ✅ Card appears, no follow-up. H2. ACTIVITY WITH FOLLOW-UP TODAY Add another comm: kind Email, follow-up = TODAY's date, body "Follow up urgent". ✅ Card appears with yellow "🔔 Follow-up " pill. ✅ Pipeline header follow-ups indicator count increases by 1 (refresh by opening Pipeline → comeback). H3. DELETE ACTIVITY ✕ on the test note → confirm. ✅ Card removed. ✅ Pipeline header indicator count drops by 1 (the H2 entry; if you deleted that one). H4. OWNER PICKER ASSIGNMENT In any Lead modal → Owner dropdown → pick "pramod" → Save. ✅ Lead card now appears under pramod's owner chip on Pipeline. ✅ By Owner table on Opps tab shows pramod with adjusted totals. H5. LEGACY-OWNER COMPATIBILITY Manually update an opp's owner in DB to a non-employee id: UPDATE sales.opportunities SET owner='ghost-id' WHERE opp_id='cm-test-opp-1'; Open the opp in PWA → Owner picker shows "ghost-id (legacy)" preselected. Picker still lets you switch to a real employee. Save. ╔═════════════════════════════════════════════════════════════════════════════╗ ║ I. MANAGED LOOKUPS + 360 + BY OWNER ║ ╚═════════════════════════════════════════════════════════════════════════════╝ I1. DATALIST AUTO-SUGGEST ➕ Add → Industry input → click into field WITHOUT typing: ✅ Browser shows all available industry values from sales.industries. Type "Re": filters to Retail, Real Estate, Residential. I2. NEW VALUE AUTO-PERSIST Type a brand-new value "Education" → Save (with valid name "Edu Test Co"). Open form again on another customer — Education appears in datalist. DB check: SELECT name FROM sales.industries WHERE name='Education'; I3. DUPLICATE GUARDED Repeat: open form, type "Education" again on a third customer. Save. ✅ No new row created (UNIQUE constraint; 409 swallowed silently). I4. CUSTOMER 360 Open Park Hospital → Info tab → 360 card values vs ground truth: Jobs: see jobs query result Offers: COUNT(*) FROM offers WHERE customer_id='cm-test-park-hospital' Open Opps: 1 (opp-1 qualification) Won: 1 (opp-6) · Lost: 0 Won value: 520000 Pipeline ₹: 425000 (opp-1 expected_value) Activity: 2 + any you added in section H Last offer + Last activity dates populate Cross-check by running: SELECT * FROM sales.customer_360 WHERE customer_id='cm-test-park-hospital'; I5. BY OWNER ACCURACY Pipeline → Opps tab → By Owner row for harish should be: Open: open opps owned by harish (≠ won, ≠ lost, is_active) Won: won opps owned by harish Pipeline ₹: SUM(expected_value) over open opps Won ₹: SUM(expected_value) over won opps Cross-check: SELECT * FROM sales.pipeline_by_owner WHERE owner_id='harish'; ╔═════════════════════════════════════════════════════════════════════════════╗ ║ J. ACCESS GATING ║ ╚═════════════════════════════════════════════════════════════════════════════╝ J1. NO-GRANT USER Hub → log out → log in as "amit" or "ashok". Navigate to /sales/customers/. ✅ "🔒 Access Restricted" screen, "Back to Hub" button. J2. GRANT VIA ADMIN As harish: /admin/ → Access tab → grant "customers" to ashok. Log in as ashok → /sales/customers/ → loads normally. J3. STALE SESSION In DevTools → Application → Local Storage → modify `lm360-session.loginAt` to a value 13 hours ago → reload PWA. ✅ Auto-redirects to /hub/ (12h session TTL). ╔═════════════════════════════════════════════════════════════════════════════╗ ║ K. NEGATIVE TESTS ║ ╚═════════════════════════════════════════════════════════════════════════════╝ K1. EMPTY CUSTOMER NAME → ❌ validation toast (done in B3). K2. EMPTY CONTACT NAME → ❌ validation toast. K3. EMPTY ADDRESS LINE1 → ❌ validation toast. K4. EMPTY DEAL NAME (Opp modal) → ❌ "Deal name is required". K5. NO CUSTOMER PICKED on new opp → ❌ "Customer is required". K6. EMPTY COMM (no subject, no body) → ❌ "Add a subject or body". K7. INVALID DATE in follow-up — Web's native date picker prevents invalid input; nothing to test in PWA logic. K8. OFFLINE — kill network in DevTools → list shows cached shell; new saves fail with err toast (no queue/offline support in Phase 1–5). K9. CONCURRENT EDITS — open the same customer in two tabs, change phone in tab1, change email in tab2. Whoever saves last wins (no conflict detection). This is a known limitation, not a bug. K10. NON-LATIN INPUT — Use Hindi text in Name "अनिल गुप्ता" — saves OK, renders OK in list and detail. ╔═════════════════════════════════════════════════════════════════════════════╗ ║ L. TEAR-DOWN ║ ╚═════════════════════════════════════════════════════════════════════════════╝ L1. Remove every customer/lead/opp/contact/address/comm you created during manual testing via the PWA's soft-delete buttons (or admin SQL). L2. Wipe the seed dataset: docker exec -i postgres psql -U lmadmin -d lm360 \ < /var/www/360lm/sales/customers/test_data_cleanup.sql Final report should show 0 in every row. L3. (Optional) Drop a quick-added customer / opp created during section E: docker exec postgres psql -U lmadmin -d lm360 -c \ "DELETE FROM sales.customers WHERE notes LIKE '%Quick-added from Sales PWA%' AND is_active=false;" ═══════════════════════════════════════════════════════════════════════════════ TEST PASS CRITERIA - All sections A → J complete with NO failures. - All negative tests in K behave as described. - Tear-down in L returns DB to clean state (test_data_cleanup verification). ═══════════════════════════════════════════════════════════════════════════════