Skip to main content
The project brief defines five non-negotiable features. Each one below is mapped to the code that implements it and to the way you can see it working, so an evaluator can check the claims rather than take them on trust. For terminology used throughout, see the glossary.

1. Customer Dashboard with live KPIs

Where: apps/api/src/modules/customers The four required cards exist, in one contract: the CustomerKpi shape declares totalCustomers, activeCustomers, totalTransactionValue and averageCustomerValue. They are real — computed as SQL aggregates over the customer table, not stored numbers that can go stale — and exposed as GET /customers/kpis. The dashboard also reads GET /customers/trend, GET /customers/health and GET /customers/top. Verify it: sign in, open the dashboard, and confirm the four cards change when you import customers or edit the data. All of these are deterministic computations — nothing here involves a language model. See AI usage.

2. Customer Segmentation with condition builder

Where: packages/contracts/src/domains/segments (the shared schema) and apps/api/src/modules/segments The condition builder covers exactly the four dimensions the brief names:
  • country — string equality.
  • totalTransactions — numeric count, compared with gt / gte / lt / lte.
  • totalAmountSpent — numeric, same operators.
  • lastActivityDays — numeric, same operators.
Conditions combine freely (AND), and the segment shows a live match count before you save, via POST /segments/preview — the same matching code runs preview and save, so the number you saw is the number you stored. Verify it: build a segment with country + totalAmountSpent and watch the match count change as you add each condition. See build a segment.

3. Campaign creation form tied to a segment

Where: packages/contracts/src/domains/campaigns and apps/api/src/modules/campaigns generateCampaignRequestSchema makes the linkage and the requirements non-negotiable at the boundary: segmentId (a reference to a saved segment, never a free-text name), objective (min 10 characters), channel (sms | email | push) and tone (four tones) are all required and enforced by the request-validation pipe before the use case runs. Verify it: try POST /campaigns/generate without a segment id, or with an unknown one, and observe the 400 — the API refuses before any generation happens. See generate a campaign.

4. AI-generated campaign output

Where: apps/api/src/modules/campaigns/application/use-cases/campaign.use-cases.ts Generation asks the model for a structured payload and the use case parses it against the campaign schema, so every generated campaign has title, message and call to action — never an empty field. If the reply does not parse, the adapter repairs what it can and surfaces llm_error otherwise; a half-hearted draft never becomes a campaign. Verify it: generate a campaign and read the three fields back from the DELETE-able list or the campaign detail. Compare the same call with and without a provider key — with no key, the deterministic local generator still returns all three fields.

5. Clean separation of the AI service layer

Where: apps/api/src/shared/infrastructure/llm/ and the port tokens in apps/api/src/shared/application/ports/ This is the architectural test. The AI lives behind a port, and the boundary is enforced by direction of dependency:
  • Controllers only validate bodies and call use cases. They never import a provider SDK or open a connection to a model gateway.
  • Use cases depend on ports — STRUCTURED_MODEL (campaigns) and TEXT_MODEL (chat) — and receive them by constructor injection.
  • Adapters — Anthropic, Gemini, OpenCode, and the deterministic scripted generator — live together in shared/infrastructure/llm/, and one of them is bound once at boot from MODEL_PROVIDER plus the available keys, wrapped by a retrying model.
  • The web app makes no model calls at all. React components call feature API functions that hit the API; a provider key never exists in the browser.
Reading it from the other side: if you add a new AI feature, you implement the port, register an adapter, and wire it in the module — you do not edit a controller or a React component. See architecture and the generative pipeline.

How to run this yourself

The full stack runs offline: pnpm db:up, pnpm seed, pnpm dev — sign in with aicha.njoya@dme.cm / campaigns. With no model key the API binds the scripted generator; add a key in apps/api/.env to switch to a hosted model. See run without a model key and the configuration reference.
Last modified on October 6, 2026