Customer Segmentation FE — Phase 2 Task Breakdown
Derived from:
customer-segmentation-fe.md· Phase 1 (UI) complete · Generated: 2026-06-30
Scope: Phase 2 — API Integration only. Actionable tasks first; fully blocked stories listed in Skipped stories.
Effort Summary
| Phase / Area | FE days | BE days | QA days | Total |
|---|---|---|---|---|
| Task 2.1 — SegmentStore spec gaps | 1 | — | 0.5 | 1.5 |
| Task 2.2 — SegmentListPage integration tests | 1.5 | — | 0.5 | 2 |
| Task 2.3 — Reachability fields (partial) | 0.5 | — | 0.5 | 1 |
| Grand total | 3 | — | 1.5 | 4.5 |
Confidence: medium. All three tasks are test-coverage work against APIs that already exist in the store — low-risk estimates. Key unknown: Task 2.3 can only go fully green once BE delivers
GET /v1/segments/:id/detailwithpercentage_reach+reachabilityfields (OQ-FE-2 in RFC§5). Until then it ships as optional-field graceful degradation.
Phase 2 — API Integration
Task 2.1: [FE] Extend SegmentStore integration tests — fetchSegments, previewSegment, archiveSegment, duplicateSegment (BA-S01, BA-S13, BA-S14)
A developer can verify that segment listing, preview, archive, and duplicate each hit the correct endpoint with the correct payload and handle errors gracefully — without running the app.
Status: ✅ Actionable
What to build
Add four new describe blocks to the existing SegmentStore.spec.ts, following the same pattern as the existing createSegment and updateSegment blocks. Also confirm the HARDCODED_SEGMENTS fallback in the fetchSegments error handler is intentional — the test should assert it (not remove it).
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | features/customers/store/SegmentStore.spec.ts | Add fetchSegments, previewSegment, archiveSegment, duplicateSegment describe blocks |
All paths verified against local repo at
/Users/mekari/Documents/CDP/qontak-customer-fe.
Implementation steps
-
Explore — Open
features/customers/store/SegmentStore.spec.ts. Read thecreateSegment — API payloadblock (lines 93–157) to understand the mock pattern:vi.stubGlobal('useCustomFetch', ...)+mockCustomFetch.mockResolvedValue(...), then assert onmockCustomFetch.mock.calls[0]. -
Write failing tests (red) — Add
describe('fetchSegments', ...):- Call
mockCustomFetch.mockResolvedValue({ data: [...], pagination: {...} }), callstore.fetchSegments(), assertmockCustomFetchwas called with{ baseURL: 'https://api.example.com', method: 'GET' }to/v1/segments - Assert
store.segmentsequalsresponse.data;store.fetchSegmentsStatus === 'resolved' - Error case:
mockCustomFetch.mockRejectedValue(...), assertfetchSegmentsStatus === 'rejected'andstore.segmentsfalls back toHARDCODED_SEGMENTS(intentional — do not remove the fallback) - Params forwarding:
store.fetchSegments({ search: 'foo', status: 'active' })→mockCustomFetchcalled withparamscontaining{ search: 'foo', status: 'active' }
- Call
-
Add
previewSegmentblock:- Assert
POST /v1/segments/previewwith{ rule_set: ... }in body - Assert
store.previewResultpopulated fromresponse.data[0];previewStatus === 'resolved' - Error:
previewStatus === 'rejected'; asserttoastNotifyis not called (current behaviour — preview fails silently)
- Assert
-
Add
archiveSegmentblock:- Assert
PATCH /v1/segments/seg-1/archivewith no body - Assert
archiveStatus === 'resolved';toastNotifycalled withvariant: 'success' - Error:
archiveStatus === 'rejected'; error toast; error re-thrown (await expect(store.archiveSegment('seg-1')).rejects.toBeDefined())
- Assert
-
Add
duplicateSegmentblock:- Assert
POST /v1/segments/seg-1/duplicate - Assert
duplicateStatus === 'resolved'; success toast - Error:
duplicateStatus === 'rejected'; error toast; error re-thrown
- Assert
-
Go green — Run
npm run test -- features/customers/store/SegmentStore.spec.tsuntil all pass. -
Quality gate — Run
npm run lint.
Acceptance criteria
-
fetchSegments:mockCustomFetchreceivesGETto/v1/segmentswith correctbaseURL;store.segmentsequalsresponse.data;fetchSegmentsStatusreachesresolved -
fetchSegmentserror:fetchSegmentsStatusreachesrejected;store.segmentsfalls back toHARDCODED_SEGMENTS(intentional safety net) -
fetchSegmentsparams:{ search: 'foo', status: 'active' }forwarded as query params -
previewSegment:POST /v1/segments/previewwithrule_setbody;store.previewResultpopulated;previewStatus === 'resolved' -
previewSegmenterror:previewStatus === 'rejected';toastNotifynot called -
archiveSegment:PATCH /v1/segments/seg-1/archive;archiveStatus === 'resolved'; success toast -
archiveSegmenterror:archiveStatus === 'rejected'; error toast; error re-thrown -
duplicateSegment:POST /v1/segments/seg-1/duplicate;duplicateStatus === 'resolved'; success toast -
duplicateSegmenterror:duplicateStatus === 'rejected'; error toast; error re-thrown -
npm run lintpasses with no new errors
Test strategy
All tests use the existing vi.stubGlobal('useCustomFetch', ...) setup and the mockCustomFetch spy already declared at the top of the spec file. Each block calls mockCustomFetch.mockResolvedValue(...) or mockCustomFetch.mockRejectedValue(...), invokes the store action, and asserts on mockCustomFetch.mock.calls[0] for request shape and store.<statusField> for lifecycle. The toastNotify mock at line 9 is already in place for toast assertions.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1 |
| QA | 0.5 |
| Total | 1.5 |
Assumptions: zero new infrastructure — pure test writing following the exact pattern of the existing 11 test cases in the same file.
Run to verify
npm run test -- features/customers/store/SegmentStore.spec.ts && npm run lint
Depends on
Nothing — fully self-contained.
Task 2.2: [FE] SegmentListPage integration tests — segment detail, customers list, archive, duplicate (BA-S15, BA-S16)
A developer can run tests that confirm the segment detail page loads correctly from the real API — showing segment info, the matched-customer table, and that archive/duplicate actions fire the right endpoints.
Status: ✅ Actionable
Design reference: https://www.figma.com/design/ZbjJxiiEsyFIBPCsTOK0lm/Customer-Segmentations?node-id=8670-238601 · DS version: @mekari/pixel3 · Frame: Segment Detail Active · Design QA: Nur Asmara / Rizky Surur
What to build
Create a new SegmentListPage.spec.ts using MSW — the same msw + create$Fetch infrastructure already in DrawerCreateSegment.spec.ts. Mock the two endpoints called on mount (GET /v1/segments/:id and GET /v1/segments/:id/customers), assert the rendered output, and cover the archive + duplicate action flows.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | features/customers/views/SegmentListPage.spec.ts | New MSW-based integration spec for segment detail page |
features/customers/views/SegmentListPage.vueconfirmed at local path. No existing spec for this view — new file.
Implementation steps
-
Explore — Open
features/customers/views/components/segment/DrawerCreateSegment.spec.ts. Note howvi.stubGlobal('useCustomFetch', () => ({ $customFetch: create$Fetch(), clearCache: vi.fn() }))wires the real fetch shim, howserver.use(http.get(...))adds MSW handlers per test, and how sub-components are stubbed inglobal.stubsto keep tests focused. -
Scaffold
SegmentListPage.spec.ts:import { beforeEach, describe, expect, it, vi } from 'vitest'import { render, screen, waitFor } from '@testing-library/vue'import { http, HttpResponse } from 'msw'import { createTestingPinia } from '@pinia/testing'import { server } from '~/tests/mocks/server'import { create$Fetch } from '~/tests/__mocks__/$fetch'import SegmentListPage from './SegmentListPage.vue'vi.stubGlobal('useCustomFetch', () => ({ $customFetch: create$Fetch(), clearCache: vi.fn() }))vi.stubGlobal('useCustomConfig', () => ({ config: { CUSTOMER_360_URL: 'https://api.example.com' } }))vi.mock('~/utils/toast', () => ({ toastNotify: vi.fn() })) -
Write failing tests (red) —
describe('on mount — loads segment detail and customers'):- Register MSW handlers in
beforeEach:http.get('*/v1/segments/seg-1', () => HttpResponse.json({ data: [{ id: 'seg-1', name: 'VIP Customers', status: 'active', total_matched: 1200, created_at: '2026-01-01' }] }))http.get('*/v1/segments/seg-1/customers', () => HttpResponse.json({ data: [{ contact_id: 'c-1', name: 'Alice', source: 'WhatsApp', added_at: '...' }], pagination: { total: 1, page: 1, per_page: 20, total_pages: 1 } }))
- Mount:
render(SegmentListPage, { props: { segmentId: 'seg-1' }, global: { plugins: [createTestingPinia({ stubActions: false })], stubs: { SegmentBreadcrumb: { template: '<div data-testid="breadcrumb"></div>', emits: ['send-campaign','edit-filter-detail','edit-filter','archive'] }, SegmentDetailTabs: true, DrawerCreateSegment: true, DrawerEditSegment: true, ModalArchiveSegment: true, ModalEditSegmentDetail: true } } }) - Assert:
await waitFor(() => { expect(screen.getByText('VIP Customers')).toBeInTheDocument() }) - Assert:
expect(screen.getByText('Alice')).toBeInTheDocument() - Assert:
expect(screen.getByText('1200')).toBeInTheDocument()(or equivalenttotal_matcheddisplay)
- Register MSW handlers in
-
Archived variant test — Register
GET /v1/segments/seg-1returningstatus: 'archived'. Assert reachability section not present (query by data-testid set in Task 2.3). -
Archive action test — Add
http.patch('*/v1/segments/seg-1/archive', () => HttpResponse.json({})). Emitarchivefrom the stubbedSegmentBreadcrumb.await waitFor(...)and assert toasttoastNotifywas called withvariant: 'success'. -
Duplicate action test — Add
http.post('*/v1/segments/seg-1/duplicate', () => HttpResponse.json({})). Trigger duplicate action. AsserttoastNotifycalled withvariant: 'success'. -
Go green —
npm run test -- features/customers/views/SegmentListPage.spec.ts -
Quality gate —
npm run lint
Acceptance criteria
- On mount with
segmentId='seg-1', component fetchesGET /v1/segments/seg-1andGET /v1/segments/seg-1/customers - Segment name 'VIP Customers' renders (from API)
-
total_matchedvalue 1200 renders in the overview section - Customer row 'Alice' with source 'WhatsApp' renders in the matched-customers table
- Archived segment: reachability section is absent from the rendered output
- Archive action fires
PATCH /v1/segments/seg-1/archive; success toast shown - Duplicate action fires
POST /v1/segments/seg-1/duplicate; success toast shown -
npm run lintpasses
Test strategy
MSW handles all HTTP; create$Fetch is the real fetch shim going through MSW, so assertions on what was called use screen queries and toast mocks rather than inspecting fetch calls directly. Sub-components that render their own fetches (SegmentDetailTabs, DrawerCreateSegment, DrawerEditSegment) are fully stubbed so they don't make network calls of their own. waitFor handles async on-mount fetches. The toastNotify mock at the top of the file asserts action outcomes.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1.5 |
| QA | 0.5 |
| Total | 2 |
Assumptions: re-uses MSW server already configured at
~/tests/mocks/serverand thecreate$Fetchhelper;SegmentListPage.vueis a large component (556 lines) so stubbing sub-components correctly will take most of the effort.
Run to verify
npm run test -- features/customers/views/SegmentListPage.spec.ts && npm run lint
Depends on
Nothing external — all APIs already exist in the codebase.
Task 2.3: [FE] Wire reachability fields — fetchSegmentDetail with percentage_reach + reachability channels (BA-S15, SEG-S11)
A user viewing an active segment's detail page sees the percentage of matched customers relative to the total contact base, plus WhatsApp and Email reachability percentages — not just a raw count.
Status: ⚠️ Partially blocked — percentage_reach and reachability.whatsapp_pct / reachability.email_pct are not in the current GET /v1/segments/:id response. The actionable portion wires the type contract and display logic now (gracefully degrading to hidden when fields are absent); the real numbers appear once OQ-FE-2 (BE API contract for GET /v1/segments/:id/detail) resolves.
Design reference: https://www.figma.com/design/ZbjJxiiEsyFIBPCsTOK0lm/Customer-Segmentations?node-id=8670-238601 · DS version: @mekari/pixel3 · Frame: Segment Detail Active · Design QA: Nur Asmara / Rizky Surur
What to build
Extend the SegmentDetail local interface in SegmentListPage.vue to accept percentageReach? and reachability?, add conditional rendering for the reachability section, and add two MSW test cases to the spec created in Task 2.2. When OQ-FE-2 resolves: change the endpoint URL and make fields required.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | features/customers/views/SegmentListPage.vue | Add percentageReach?, reachability? to SegmentDetail type; map from API response; conditional v-if rendering |
| extend | features/customers/views/SegmentListPage.spec.ts | Add two MSW test cases: reachability present vs absent |
Implementation steps
-
Explore — Open
features/customers/views/SegmentListPage.vue. Find theSegmentDetailref around line 294 andfetchSegmentDetailaround line 522. Note the current response type:{ id, name, description, status, total_matched?, created_at? }— no reachability fields yet. -
Extend the interface (around line 294):
const segmentDetail = ref<{description: stringstatus: 'active' | 'archived'totalMatched?: numbercreatedAt?: stringpercentageReach?: numberreachability?: { whatsapp: number; email: number }} | undefined>(undefined) -
Update
fetchSegmentDetail(line 522) — extend response type and map fields:const response = await $customFetch<{data: Array<{id: string; name: string; description: stringstatus: 'active' | 'archived'; total_matched?: number; created_at?: stringpercentage_reach?: numberreachability?: { whatsapp_pct: number; email_pct: number }}>}>(`/v1/segments/${props.segmentId}`, { baseURL: config.CUSTOMER_360_URL, method: 'GET' })// in the mapping block, add:segmentDetail.value = {...segmentDetail.value,percentageReach: segment.percentage_reach,reachability: segment.reachability? { whatsapp: segment.reachability.whatsapp_pct, email: segment.reachability.email_pct }: undefined,}Keep the endpoint as
/v1/segments/:idfor now — stub the fields with optional chaining until OQ-FE-2 resolves. -
Add conditional rendering in the template overview section:
<template v-if="segmentDetail?.status === 'active' && segmentDetail?.percentageReach !== undefined"><!-- percentage reach: {{ segmentDetail.percentageReach }}% --></template><template v-if="segmentDetail?.status === 'active' && segmentDetail?.reachability"><!-- WhatsApp: {{ segmentDetail.reachability.whatsapp }}% --><!-- Email: {{ segmentDetail.reachability.email }}% --></template>Add
data-testid="reachability-section"to the outer wrapper for spec queries. -
Extend
SegmentListPage.spec.ts(Task 2.2's file) — add two newitcases inside the existingdescribe:- With reachability:
GET /v1/segments/seg-1returns{ ..., percentage_reach: 45, reachability: { whatsapp_pct: 30, email_pct: 15 } }. Assertscreen.getByText('45%')(or equivalent) is in the document. - Without reachability (existing handler, no extra fields): Assert
queryByTestId('reachability-section')is null.
- With reachability:
-
Go green —
npm run test -- features/customers/views/SegmentListPage.spec.ts -
Once OQ-FE-2 resolves:
- Swap endpoint to
/v1/segments/${props.segmentId}/detail - Make
percentageReachandreachabilityrequired (remove?) - Update spec handler to use the new endpoint path
- Remove optional-chaining guards from the template
- Swap endpoint to
Acceptance criteria
-
SegmentDetailtype acceptspercentageReach?: numberandreachability?: { whatsapp: number; email: number }— TypeScript compiles clean withnpm run build - When API returns
percentage_reach: 45: overview section shows 45% reach - When API returns
reachability.whatsapp_pct: 30: WhatsApp reachability renders 30% - When API returns no
percentage_reach: reachability section is absent - Archived segment: reachability section absent regardless of API response
-
npm run test -- features/customers/views/SegmentListPage.spec.tspasses - Pending OQ-FE-2: endpoint is
GET /v1/segments/:id/detail; fields are required (non-optional)
Test strategy
Two MSW scenario cases added to the existing SegmentListPage.spec.ts describe block: one handler returns percentage_reach + reachability, the other omits them. Assert on rendered text values and the presence/absence of data-testid="reachability-section". The archived-segment case (from Task 2.2 step 4) confirms the section is absent regardless of API data.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| QA | 0.5 |
| Total | 1 |
Assumptions: type-only change + two
v-ifwrappers in the template — no new component; follows the exact optional-field rendering pattern already used elsewhere inSegmentListPage.vue; the blocked half is a 1-line URL swap + type tightening once BE ships.
Run to verify
npm run test -- features/customers/views/SegmentListPage.spec.ts && npm run lint && npm run build
Depends on
- Task 2.2 (
SegmentListPage.spec.tsmust exist before this task extends it) - External — OQ-FE-2: BE confirms
GET /v1/segments/:id/detailresponse shape includingpercentage_reachandreachabilityfields
Ordering rationale
- 2.1 first — pure store-level spec, zero external deps; unblocks QA sign-off on all existing store actions and surfaces any payload mismatches (e.g., confirms
archiveSegmentusesPATCH, notPOST) before the view-level work lands. - 2.2 second — creates the
SegmentListPage.spec.tsfile that Task 2.3 depends on; also sets up the MSW handler scaffold once so Task 2.3 just adds two new cases. - 2.3 last — depends on 2.2's spec file; actionable half ships in the same sprint; blocked half (
GET .../detailendpoint swap) is a follow-on 1-hour PR once OQ-FE-2 resolves. - Critical external push: OQ-FE-2 — BE team must confirm
GET /v1/segments/:id/detailresponse shape (or confirmGET /v1/segments/:idwill include those fields). Everything else is unblocked today.
Discovered Phase 1 gap (not a Phase 2 task): features/customers/views/ListPage.vue has zero references to "segment" — the Customer Index sub-sidenav Segments section (BA-S02, RFC Chunk 2) has not been built. This is Phase 1 work; track it separately or add it to the current sprint before Phase 2 work for that screen begins.
Skipped stories
| Story | Reason |
|---|---|
| SEG-S02..S06 (Association Conditions API integration) | Phase 1 (AssociationCondition.vue) not started — blocked on Figma (OQ-FE-1 in RFC §5) |
| SEG-S09 (Aggregate Metrics API integration) | Phase 1 (AggregateMetricCondition.vue) not started — blocked on Figma + BE endpoint TBD |
| BA-S17 / SEG-S13 (Performance Tab API) | Phase 1 (PerformanceTab.vue) not started — blocked on Figma + GET /v1/segments/:id/performance endpoint |
| BA-S18 (Send Campaign — Broadcast API) | Phase 1 (SendCampaignDrawer.vue) not started — blocked on Figma + Broadcast API contract (OQ-FE-4 in RFC §5) |
| BA-S02 (Customer Index sub-sidenav) | Phase 1 not done — ListPage.vue has no segment section; resolve Phase 1 gap first |