Skip to main content

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 / AreaFE daysBE daysQA daysTotal
Task 2.1 — SegmentStore spec gaps10.51.5
Task 2.2 — SegmentListPage integration tests1.50.52
Task 2.3 — Reachability fields (partial)0.50.51
Grand total31.54.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/detail with percentage_reach + reachability fields (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

ActionFileWhat changes
extendfeatures/customers/store/SegmentStore.spec.tsAdd fetchSegments, previewSegment, archiveSegment, duplicateSegment describe blocks

All paths verified against local repo at /Users/mekari/Documents/CDP/qontak-customer-fe.

Implementation steps

  1. Explore — Open features/customers/store/SegmentStore.spec.ts. Read the createSegment — API payload block (lines 93–157) to understand the mock pattern: vi.stubGlobal('useCustomFetch', ...) + mockCustomFetch.mockResolvedValue(...), then assert on mockCustomFetch.mock.calls[0].

  2. Write failing tests (red) — Add describe('fetchSegments', ...):

    • Call mockCustomFetch.mockResolvedValue({ data: [...], pagination: {...} }), call store.fetchSegments(), assert mockCustomFetch was called with { baseURL: 'https://api.example.com', method: 'GET' } to /v1/segments
    • Assert store.segments equals response.data; store.fetchSegmentsStatus === 'resolved'
    • Error case: mockCustomFetch.mockRejectedValue(...), assert fetchSegmentsStatus === 'rejected' and store.segments falls back to HARDCODED_SEGMENTS (intentional — do not remove the fallback)
    • Params forwarding: store.fetchSegments({ search: 'foo', status: 'active' })mockCustomFetch called with params containing { search: 'foo', status: 'active' }
  3. Add previewSegment block:

    • Assert POST /v1/segments/preview with { rule_set: ... } in body
    • Assert store.previewResult populated from response.data[0]; previewStatus === 'resolved'
    • Error: previewStatus === 'rejected'; assert toastNotify is not called (current behaviour — preview fails silently)
  4. Add archiveSegment block:

    • Assert PATCH /v1/segments/seg-1/archive with no body
    • Assert archiveStatus === 'resolved'; toastNotify called with variant: 'success'
    • Error: archiveStatus === 'rejected'; error toast; error re-thrown (await expect(store.archiveSegment('seg-1')).rejects.toBeDefined())
  5. Add duplicateSegment block:

    • Assert POST /v1/segments/seg-1/duplicate
    • Assert duplicateStatus === 'resolved'; success toast
    • Error: duplicateStatus === 'rejected'; error toast; error re-thrown
  6. Go green — Run npm run test -- features/customers/store/SegmentStore.spec.ts until all pass.

  7. Quality gate — Run npm run lint.

Acceptance criteria

  • fetchSegments: mockCustomFetch receives GET to /v1/segments with correct baseURL; store.segments equals response.data; fetchSegmentsStatus reaches resolved
  • fetchSegments error: fetchSegmentsStatus reaches rejected; store.segments falls back to HARDCODED_SEGMENTS (intentional safety net)
  • fetchSegments params: { search: 'foo', status: 'active' } forwarded as query params
  • previewSegment: POST /v1/segments/preview with rule_set body; store.previewResult populated; previewStatus === 'resolved'
  • previewSegment error: previewStatus === 'rejected'; toastNotify not called
  • archiveSegment: PATCH /v1/segments/seg-1/archive; archiveStatus === 'resolved'; success toast
  • archiveSegment error: archiveStatus === 'rejected'; error toast; error re-thrown
  • duplicateSegment: POST /v1/segments/seg-1/duplicate; duplicateStatus === 'resolved'; success toast
  • duplicateSegment error: duplicateStatus === 'rejected'; error toast; error re-thrown
  • npm run lint passes 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

DisciplineDays
Frontend1
QA0.5
Total1.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

ActionFileWhat changes
createfeatures/customers/views/SegmentListPage.spec.tsNew MSW-based integration spec for segment detail page

features/customers/views/SegmentListPage.vue confirmed at local path. No existing spec for this view — new file.

Implementation steps

  1. Explore — Open features/customers/views/components/segment/DrawerCreateSegment.spec.ts. Note how vi.stubGlobal('useCustomFetch', () => ({ $customFetch: create$Fetch(), clearCache: vi.fn() })) wires the real fetch shim, how server.use(http.get(...)) adds MSW handlers per test, and how sub-components are stubbed in global.stubs to keep tests focused.

  2. 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() }))
  3. 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 equivalent total_matched display)
  4. Archived variant test — Register GET /v1/segments/seg-1 returning status: 'archived'. Assert reachability section not present (query by data-testid set in Task 2.3).

  5. Archive action test — Add http.patch('*/v1/segments/seg-1/archive', () => HttpResponse.json({})). Emit archive from the stubbed SegmentBreadcrumb. await waitFor(...) and assert toast toastNotify was called with variant: 'success'.

  6. Duplicate action test — Add http.post('*/v1/segments/seg-1/duplicate', () => HttpResponse.json({})). Trigger duplicate action. Assert toastNotify called with variant: 'success'.

  7. Go greennpm run test -- features/customers/views/SegmentListPage.spec.ts

  8. Quality gatenpm run lint

Acceptance criteria

  • On mount with segmentId='seg-1', component fetches GET /v1/segments/seg-1 and GET /v1/segments/seg-1/customers
  • Segment name 'VIP Customers' renders (from API)
  • total_matched value 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 lint passes

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

DisciplineDays
Frontend1.5
QA0.5
Total2

Assumptions: re-uses MSW server already configured at ~/tests/mocks/server and the create$Fetch helper; SegmentListPage.vue is 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

ActionFileWhat changes
extendfeatures/customers/views/SegmentListPage.vueAdd percentageReach?, reachability? to SegmentDetail type; map from API response; conditional v-if rendering
extendfeatures/customers/views/SegmentListPage.spec.tsAdd two MSW test cases: reachability present vs absent

Implementation steps

  1. Explore — Open features/customers/views/SegmentListPage.vue. Find the SegmentDetail ref around line 294 and fetchSegmentDetail around line 522. Note the current response type: { id, name, description, status, total_matched?, created_at? } — no reachability fields yet.

  2. Extend the interface (around line 294):

    const segmentDetail = ref<{
    description: string
    status: 'active' | 'archived'
    totalMatched?: number
    createdAt?: string
    percentageReach?: number
    reachability?: { whatsapp: number; email: number }
    } | undefined>(undefined)
  3. Update fetchSegmentDetail (line 522) — extend response type and map fields:

    const response = await $customFetch<{
    data: Array<{
    id: string; name: string; description: string
    status: 'active' | 'archived'; total_matched?: number; created_at?: string
    percentage_reach?: number
    reachability?: { 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/:id for now — stub the fields with optional chaining until OQ-FE-2 resolves.

  4. 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.

  5. Extend SegmentListPage.spec.ts (Task 2.2's file) — add two new it cases inside the existing describe:

    • With reachability: GET /v1/segments/seg-1 returns { ..., percentage_reach: 45, reachability: { whatsapp_pct: 30, email_pct: 15 } }. Assert screen.getByText('45%') (or equivalent) is in the document.
    • Without reachability (existing handler, no extra fields): Assert queryByTestId('reachability-section') is null.
  6. Go greennpm run test -- features/customers/views/SegmentListPage.spec.ts

  7. Once OQ-FE-2 resolves:

    • Swap endpoint to /v1/segments/${props.segmentId}/detail
    • Make percentageReach and reachability required (remove ?)
    • Update spec handler to use the new endpoint path
    • Remove optional-chaining guards from the template

Acceptance criteria

  • SegmentDetail type accepts percentageReach?: number and reachability?: { whatsapp: number; email: number } — TypeScript compiles clean with npm 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.ts passes
  • 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

DisciplineDays
Frontend0.5
QA0.5
Total1

Assumptions: type-only change + two v-if wrappers in the template — no new component; follows the exact optional-field rendering pattern already used elsewhere in SegmentListPage.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.ts must exist before this task extends it)
  • External — OQ-FE-2: BE confirms GET /v1/segments/:id/detail response shape including percentage_reach and reachability fields

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 archiveSegment uses PATCH, not POST) before the view-level work lands.
  • 2.2 second — creates the SegmentListPage.spec.ts file 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 .../detail endpoint 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/detail response shape (or confirm GET /v1/segments/:id will 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

StoryReason
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