# Kuwait Closed-Loop Prepaid Card System
## Proposal: Gap Analysis & Implementation Plan

**Branch:** `prepaid-card-system`
**Date:** April 2026
**Prepared for:** Kuwait Opportunity — Closed-Loop Benefit Card Program
**Document Type:** Technical Proposal — Internal Reference

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Kuwait Program Requirements](#2-kuwait-program-requirements)
3. [Existing System Capabilities](#3-existing-system-capabilities)
4. [Gap Analysis](#4-gap-analysis)
5. [Wallet-as-Card Architecture](#5-wallet-as-card-architecture)
6. [Implementation Plan](#6-implementation-plan)
7. [Naming & Translation Strategy](#7-naming--translation-strategy)
8. [Infrastructure Requirements (On-Premise)](#8-infrastructure-requirements-on-premise)
9. [OPEX Pricing Model](#9-opex-pricing-model)
10. [Risk & Timeline Summary](#10-risk--timeline-summary)

---

## 1. Executive Summary

The existing **MomentPay wallet platform** (Elixir/Phoenix umbrella, 29 apps, 47 DB tables) is a strong fit for the Kuwait closed-loop prepaid card program. The core payment engine, card lifecycle, admin console, compliance, and reporting infrastructure are all production-ready.

Approximately **60% of requirements are already met** by the existing platform. The remaining 40% are targeted additions — not architectural rebuilds — centred on:

- A new **`wallet_prepaid` domain app** for Kuwait-specific program management
- **Bilingual fields** (Arabic name, National ID) on cardholder records
- **Closed-loop merchant restriction** (purchase only at approved cooperatives)
- **Bulk card issuance** from a client-provided CSV list (14-day SLA)
- **Statement-driven batch top-up** (2-day SLA)
- **Arabic UI** with RTL layout support
- **On-premise deployment** packaging

Estimated delivery: **18–22 working days** of focused development across 10 sprints.

---

## 2. Kuwait Program Requirements

### 2.1 Functional Requirements

| # | Requirement | Category |
|---|---|---|
| F01 | Company specialised in payment technology, card issuance & allocation | Qualification |
| F02 | Capability to top up cards using transferred funds | Core Feature |
| F03 | Fully integrated payment and disbursement system | Core Feature |
| F04 | Monthly top-up per funds/statement sent by client | Batch Processing |
| F05 | Expected card volume: 5,000–6,000 cards | Scalability |
| F06 | Comprehensive real-time reports on card activity and transactions | Reporting |
| F07 | Cards filled within **2 days** of receiving the statement | SLA |
| F08 | All remaining balances settled and refunded | Balance Management |
| F09 | Beneficiaries can purchase from company's sales centres and outlets | Closed-Loop Network |
| F10 | Cards issued within **2 weeks** of receiving issuance letter | SLA |
| F11 | Cardholder list: Name (Arabic + English), Phone, National ID, Top-Up Amount | Data/Export |
| F12 | Cardholder can change PIN | Self-Service |
| F13 | Permission to monitor disbursement and purchase operations; account statements | Admin Monitoring |
| F14 | System linked to all transactions: name, location of purchase | Transaction Data |
| F15 | Withdrawals limited to cooperative societies in Kuwait and/or designated entities | Restriction |
| F16 | Monthly report: all card balances before next transfer | Reporting |

### 2.2 Non-Functional Requirements

| # | Requirement |
|---|---|
| NF01 | On-premise hosting (client's infrastructure) |
| NF02 | OPEX pricing model |
| NF03 | Arabic language support in cardholder-facing communications |
| NF04 | Data security for National ID, PIN, and personal data |

---

## 3. Existing System Capabilities

The following capabilities are **already implemented** in the codebase (confirmed by source code review of the `prepaid-card-system` branch).

### 3.1 Umbrella App Inventory

| App | Key Capability | Relevant Source Files |
|---|---|---|
| `wallet_cards` | Card lifecycle (virtual/linked), CardStore (ETS+MySQL), 8 commands | `commands/request_virtual_card.ex`, `commands/block_card.ex`, `commands/freeze_card.ex`, `commands/reset_card_pin.ex`, `commands/set_card_limits.ex`, `commands/sync_card_balance.ex` |
| `wallet_auth` | JWT, OTP/MFA, **PIN change**, PIN reset with OTP | `pin/commands/change_pin.ex` ✅, `pin/commands/create_pin.ex`, `pin/commands/pin_reset_with_otp.ex` |
| `wallet_accounts` | Account lifecycle, sub-wallet, virtual IBANs | Account CRUD, OpenWalletAccount, sub-wallet support |
| `wallet_ledger` | Double-entry ledger, balance queries | `GetSubWalletBalance`, `ApplyCredit`, `TransferBetweenSubWallets` |
| `wallet_transfers` | Transfer state machine, inbound credit routing | `ProcessCashIn`, `InboundCreditRouter` |
| `wallet_wps` | **Batch salary credits** (reusable pattern) | `commands/batch_post_salary_credits.ex`, `wps_parser.ex`, `wps_file.ex` |
| `wallet_reporting` | Report generation, scheduling, CSV/PDF export | `commands/schedule_report.ex`, `commands/generate_report.ex`, `commands/request_report.ex` |
| `wallet_merchant` | Merchant profiles, POS payments, QR | `commands/authorize_pos_payment.ex`, `commands/capture_pos_payment.ex` |
| `wallet_limits_fees` | Daily/monthly limit policies, fee engine | `limit_policy.ex`, `policy_engine.ex`, `queries/evaluate_transfer_limits.ex` |
| `wallet_compliance` | KYC, AML, SAR, data governance | Full compliance pipeline |
| `wallet_web` | Admin console (26+ LiveViews), customer UI | `admin/card_search_live.ex`, `admin/transaction_inquiry_live.ex` |
| `wallet_settlement` | Settlement batches, reconciliation | Settlement pipeline |
| `wallet_database` | Canonical Ecto Repo, 47 tables, write-through | MySQL, all schemas |

### 3.2 Admin Card Console (Confirmed Running)

The screenshot at `demo.ctrmv.com:24000/admin/cards` confirms:
- **Total Issued / Active / Blocked / Linked External** stat cards
- **Issued Cards** table: User, Card (masked), Network, Status, Expiry, Virtual
- **Linked Cards** tab: External linked card management
- **Block/Unblock** admin actions with reason and confirmation
- **Status + Type** filters

This screen is fully implemented and can be extended for prepaid program management.

---

## 4. Gap Analysis

### 4.1 Functional Gap Table

| Requirement | Status | What Exists | What Is Missing |
|---|---|---|---|
| **F01** Card issuance & allocation | ✅ Covered | `RequestVirtualCard` command; `CardStore`; admin card search UI | Bulk issuance endpoint; physical card provisioning |
| **F02** Top-up via transferred funds | ⚠️ Partial | `ProcessCashIn`; IBAN routing; `InboundCreditRouter` | Direct `TopUpCard` command linking inbound credit → card balance; no inline card credit command |
| **F03** Integrated payment & disbursement | ✅ Covered | Full ledger + settlement + transfer pipeline | Config for closed-loop disbursement only |
| **F04** Monthly batch top-up per statement | ⚠️ Partial | `wallet_wps` `BatchPostSalaryCredits` (reusable pattern); `WpsParser` for file ingestion | `BatchTopUpCards` command adapted for prepaid; 2-day SLA enforcement |
| **F05** 5,000–6,000 card volume | ✅ Covered | ETS + MySQL backend scales comfortably | Load test recommended |
| **F06** Real-time card activity reports | ⚠️ Partial | `TransactionInquiryLive`; `AuditLogLive`; `StatementExportService` | Aggregated card portfolio report across all 5–6k cards; real-time dashboard per program |
| **F07** Cards topped up within 2 days of statement | ❌ Gap | No SLA timer | 48-hour SLA enforcement on batch top-up; auto-escalation on breach |
| **F08** Remaining balances settled/refunded | ⚠️ Partial | `wallet_disputes` refund pipeline; `CompensateP2aTransfer` | `SweepProgramBalances` command to bulk-zero all card balances to sponsor account |
| **F09** Purchase from designated outlets only | ❌ Gap | `wallet_merchant` has open merchant network | **Closed-loop allowlist**: `PrepaidProgramMerchants` table; pre-authorisation policy check |
| **F10** Cards issued within 2 weeks of letter | ❌ Gap | `RequestVirtualCard` (single card only) | **Bulk issuance** from CSV; 14-day SLA tracker per issuance batch |
| **F11** Cardholder list (Arabic+English, Phone, National ID, amount) | ❌ Gap | `display_name` only (single field, English); no `national_id` field | `name_en`, `name_ar`, `national_id` (encrypted) fields; card list export command |
| **F12** Cardholder can change PIN | ⚠️ Partial | `WalletAuth.Pin.Commands.ChangePIN` exists ✅; `ResetCardPin` (admin/OTP recovery) | **Self-service PIN change LiveView** for cardholders; current_pin → new_pin flow |
| **F13** Monitor disbursement; account statements | ✅ Covered | `TransactionInquiryLive`, `AuditLogLive`, `StatementsLive`, `StatementExportService` | Role-gated access already in place |
| **F14** Transactions linked to name + purchase location | ⚠️ Partial | Ledger entries reference `user_id`; merchant stores location | Denormalised report joining cardholder name + merchant geo in one export |
| **F15** Withdrawals limited to cooperatives in Kuwait | ❌ Gap | No allowlist enforcement at authorisation time | `ClosedLoopPolicy` check in `AuthorizePosPayment` pipeline; merchant allowlist admin UI |
| **F16** Monthly balance report before next transfer | ⚠️ Partial | `ScheduleReport`, `GenerateReport` exist; per-card balance queryable | **Monthly programme-wide balance report**: all 5–6k cards, name_en/ar, national_id, phone, balance |

### 4.2 Non-Functional Gap Table

| Requirement | Status | What Exists | What Is Missing |
|---|---|---|---|
| **NF01** On-premise hosting | ❌ Gap | Standard Elixir/Phoenix (no cloud lock-in) | Docker Compose / systemd deployment manifests; Nginx reverse proxy config; database backup scripts |
| **NF02** OPEX pricing model | N/A (Commercial) | — | Per-card/month pricing tiers defined in proposal |
| **NF03** Arabic language support | ❌ Gap | Gettext configured (`DaProductAppWeb.Gettext`); `ar` locale not created | `priv/gettext/ar/` locale files; RTL CSS (`dir="rtl"`); bilingual name fields |
| **NF04** Data security (National ID, PIN, PII) | ⚠️ Partial | PIN encrypted (bcrypt); `DataGovernance` module; `WalletDatabase.Types.EncryptedDeterministic` available | `national_id` field must use `EncryptedDeterministic` type; encryption-at-rest validation for new fields |

### 4.3 Scorecard Summary

| Category | Score | Priority |
|---|---|---|
| Core payment & card infrastructure | ✅ Ready | — |
| Admin monitoring & audit trail | ✅ Ready | — |
| KYC / AML / compliance pipeline | ✅ Ready | — |
| PIN change (self-service) | ✅ Command exists, UI missing | P1 |
| Card top-up via inbound transfer | ⚠️ Wire-up needed | P0 |
| Statement-driven batch top-up (2-day SLA) | ⚠️ Pattern exists (WPS), adapt needed | P0 |
| Remaining balance sweep | ⚠️ Partial refund logic, sweep missing | P0 |
| Monthly balance report (all cards) | ⚠️ Engine exists, programme query missing | P0 |
| Bulk card issuance + 14-day SLA | ❌ Not present | P0 |
| Arabic name + National ID fields | ❌ Not present | P0 |
| Closed-loop merchant allowlist | ❌ Not present | P0 |
| Arabic / RTL UI | ❌ Not present | P1 |
| On-premise deployment packaging | ❌ Not present | P1 |

---

## 5. Wallet-as-Card Architecture

### 5.1 Design Philosophy

Rather than building a separate card system, the Kuwait program leverages the existing **wallet-as-card** design already present in the codebase. Evidence: `CardBalanceSyncRecord` struct in `wallet_cards` already bridges `card_balance ↔ wallet_balance`, proving this was the intended architecture.

```
Each beneficiary = one cardholder record
                 + one wallet account  (holds KWD balance)
                 + one virtual card    (payment instrument)
                 + CardBalanceSyncRecord (keeps both in sync)
```

### 5.2 Prepaid Program Model

```
PrepaidProgram (new)
│   name_en: "Kuwait Food Benefits Card"
│   name_ar: "بطاقة مزايا الغذاء الكويتية"
│   sponsor_name: "Ministry of Social Affairs"
│   currency_code: KWD
│   top_up_amount: 50.000
│   max_cards: 6000
│
├── PrepaidCardholder (new — extends existing user)
│       name_en, name_ar, phone, national_id (encrypted)
│
├── Card (existing wallet_cards)
│       is_prepaid: true
│       program_id: (FK to PrepaidProgram)
│       backed by sub-wallet balance
│
├── PrepaidProgramMerchant (new — closed-loop allowlist)
│       merchant_id, name_en, name_ar, location, merchant_type
│
├── IssuanceBatch (new — SLA tracking)
│       letter_received_at, deadline_at (+14 days)
│
└── TopUpBatch (new — SLA tracking)
        statement_received_at, deadline_at (+2 days)
```

### 5.3 Transaction Flow

```
Monthly Top-Up:
  Client sends statement → BatchTopUpCards command
  → for each card: ApplyCredit (wallet_ledger)
  → SyncCardBalance (wallet_cards)
  → SLA: must complete within 48h of statement receipt

Purchase (Closed-Loop):
  Cardholder taps card at cooperative →
  ClosedLoopPolicy.authorize(program_id, merchant_id)
  → if merchant in PrepaidProgramMerchants: proceed
  → if not: decline with reason :merchant_not_in_program
  → AuthorizePosPayment (wallet_merchant)
  → DebitWalletBalance (wallet_ledger)

Balance Sweep:
  Admin triggers SweepProgramBalances →
  for each card with balance > 0:
    TransferBetweenSubWallets (card → sponsor account)
  → generates sweep summary report
```

---

## 6. Implementation Plan

### New Deliverable: `apps/wallet_prepaid`

A new umbrella app following the exact same pattern as all 29 existing apps:
- `mix.exs` with `in_umbrella: true`
- ETS GenServer stores + write-through persistence
- Commands that emit domain events + audit events
- Tests using `ExUnit.Case, async: false` + store reset

---

### Sprint 0 — New App Scaffold (0.5 day)

**Files to create:**
- `apps/wallet_prepaid/mix.exs`
- `apps/wallet_prepaid/lib/wallet_prepaid.ex`
- `apps/wallet_prepaid/lib/wallet_prepaid/application.ex`

**Root `mix.exs` change:**
- Add `{:wallet_prepaid, in_umbrella: true}` and `{:nimble_csv, "~> 1.2"}` to root deps

**Dependencies:** `wallet_cards`, `wallet_accounts`, `wallet_ledger`, `wallet_database`, `wallet_shared_kernel`, `wallet_merchant`, `nimble_csv`

---

### Sprint 1 — Database Migrations (1 day)

All migrations go in `apps/wallet_database/priv/repo/migrations/` following the `20260418NNNNNN_` timestamp format.

| Migration | Table | Key Columns |
|---|---|---|
| `20260418000001_add_bilingual_fields_to_users.exs` | `users` (ALTER) | `name_en`, `name_ar`, `national_id` (EncryptedDeterministic + index) |
| `20260418000002_create_prepaid_programs.exs` | `prepaid_programs` | `program_code` (unique), `name_en`, `name_ar`, `sponsor_name`, `sponsor_name_ar`, `country_code` (KW), `currency_code` (KWD), `max_cards`, `top_up_amount`, `top_up_schedule`, `status`, `letter_received_at` |
| `20260418000003_add_program_id_to_cards.exs` | `cards` (ALTER) | `program_id` (FK), `is_prepaid` (bool), `top_up_amount` |
| `20260418000004_create_prepaid_program_merchants.exs` | `prepaid_program_merchants` | `program_id` (FK), `merchant_id` (FK), `name_en`, `name_ar`, `location`, `merchant_type` (cooperative/outlet/designated) |
| `20260418000005_create_prepaid_issuance_batches.exs` | `prepaid_issuance_batches` | `program_id`, `total_count`, `issued_count`, `failed_count`, `status`, `letter_received_at`, `deadline_at` (+14 days) |
| `20260418000006_create_prepaid_top_up_batches.exs` | `prepaid_top_up_batches` | `program_id`, `total_count`, `completed_count`, `total_amount`, `status`, `statement_received_at`, `deadline_at` (+2 days) |

**Ecto Schema changes:**
- `WalletDatabase.Schemas.Auth.User` — add `name_en`, `name_ar`, `national_id` fields
- `WalletDatabase.Schemas.Cards.Card` — add `program_id`, `is_prepaid`, `top_up_amount` fields

---

### Sprint 2 — PrepaidProgram Domain (1.5 days)

**New files in `apps/wallet_prepaid/lib/wallet_prepaid/`:**

```
prepaid_program.ex              — struct, TypedId prefix "prog_"
program_store.ex                — ETS GenServer: store/get/list_all/list_by_status/reset
program_persistence.ex          — WriteThrough to prepaid_programs table
program_merchant_store.ex       — ETS: store/get/list_by_program/reset
program_merchant_persistence.ex — WriteThrough to prepaid_program_merchants table
closed_loop_policy.ex           — authorize_purchase(program_id, merchant_id) → :allow | {:deny, reason}
commands/create_prepaid_program.ex   — validates KWD currency, emits PrepaidProgramCreated
commands/update_prepaid_program.ex   — status transitions (draft→active→closed)
commands/add_program_merchant.ex     — validates merchant via WalletMerchant.MerchantStore, emits event
commands/remove_program_merchant.ex  — soft-disable, emits event
events/prepaid_program_created.ex
events/prepaid_program_updated.ex
events/program_merchant_added.ex
events/program_merchant_removed.ex
```

**Key function signatures:**

```elixir
WalletPrepaid.ClosedLoopPolicy.authorize_purchase(program_id, merchant_id)
  → :allow | {:deny, :merchant_not_in_program} | {:deny, :program_not_found}

WalletPrepaid.Commands.CreatePrepaidProgram.execute(%{
  name_en: String.t(), name_ar: String.t(), sponsor_name: String.t(),
  currency_code: "KWD", max_cards: integer(), top_up_amount: Decimal.t()
})
  → {:ok, %PrepaidProgram{}} | {:error, reason}
```

**Tests:** `apps/wallet_prepaid/test/program_store_test.exs`, `commands/create_prepaid_program_test.exs`, `closed_loop_policy_test.exs`

---

### Sprint 3 — Bulk Issuance Pipeline (2 days)

**New files:**

```
bulk_issuance/beneficiary_row.ex    — struct: name_en, name_ar, phone, national_id, top_up_amount
bulk_issuance/csv_parser.ex         — NimbleCSV; validates required fields; row-level errors with line numbers
commands/bulk_issue_cards.ex        — orchestrates: parse → upsert user → open account → RequestVirtualCard
events/bulk_issuance_started.ex
events/bulk_issuance_completed.ex   — payload: {program_id, issued_count, failed_count, batch_id}
events/card_issuance_failed.ex
```

**CSV Format (Issuance):**

```
name_en,name_ar,phone,national_id,top_up_amount
John Smith,جون سميث,+96550000001,123456789,50.000
```

**`BulkIssueCards.execute/1` flow:**

```
1. CsvParser.parse(binary) → [{:ok, BeneficiaryRow}] | {:error, errors}
2. CreateIssuanceBatch(program_id, initiated_by, letter_received_at)
   → deadline_at = letter_received_at + 14 days
3. For each row (idempotent by national_id + program_id):
   a. Upsert user with name_en, name_ar, phone, national_id (encrypted)
   b. OpenWalletAccount (wallet_accounts)
   c. RequestVirtualCard (wallet_cards) with is_prepaid: true, program_id
4. Update IssuanceBatch: issued_count, failed_count, status, completed_at
5. Emit BulkIssuanceCompleted
```

**Idempotency:** Re-running for same national_id + program_id skips already-issued cards.

**Tests:** `csv_parser_test.exs`, `bulk_issue_cards_test.exs` (mocked WalletCards adapter)

---

### Sprint 4 — Batch Top-Up + Balance Sweep (1.5 days)

**Reuse pattern from:** `apps/wallet_wps/lib/wallet_wps/commands/batch_post_salary_credits.ex`

**New files:**

```
top_up/statement_parser.ex          — parse CSV: national_id, amount
commands/batch_top_up_from_statement.ex
commands/sweep_program_balances.ex
events/batch_top_up_started.ex
events/batch_top_up_completed.ex    — {program_id, topped_up_count, total_amount, sla_deadline}
events/balance_sweep_completed.ex   — {program_id, swept_count, total_swept_amount}
```

**Top-Up CSV Format (Statement):**

```
national_id,amount
123456789,50.000
987654321,75.000
```

**`BatchTopUpFromStatement.execute/1` flow:**

```
1. StatementParser.parse(binary) → [{national_id, amount}]
2. CreateTopUpBatch(program_id, initiated_by, statement_received_at)
   → deadline_at = statement_received_at + 2 days (SLA)
3. For each row:
   a. Lookup card by national_id within program
   b. WalletLedger.Commands.ApplyCredit to card sub-wallet
   c. WalletCards.Commands.SyncCardBalance
   d. Post CardTransaction record (type: :top_up, reference idempotent)
4. Update TopUpBatch: completed_count, failed_count, total_amount, status
5. Emit BatchTopUpCompleted with sla_deadline
```

**`SweepProgramBalances.execute/1` flow:**

```
1. List all active cards in program with balance > 0
2. For each card:
   WalletTransfers.Commands.TransferBetweenSubWallets(
     from: card_sub_wallet,
     to: sponsor_sub_wallet,
     amount: card.balance
   )
3. Emit BalanceSweepCompleted{swept_count, total_swept_amount}
```

---

### Sprint 5 — Monthly Balance Report (0.5 day)

**New file:**

```
reporting/monthly_balance_report.ex
commands/generate_monthly_balance_report.ex
events/monthly_balance_report_generated.ex
```

**Report Columns:**

| Column | Source |
|---|---|
| Name (English) | `user.name_en` |
| Name (Arabic) | `user.name_ar` |
| National ID | `user.national_id` (decrypted) |
| Phone | `user.phone_number` |
| Card Last 4 | `card.last_four` |
| Card Status | `card.status` |
| Balance (KWD) | `GetSubWalletBalance.execute(card.account_id)` |
| Last Top-Up | `card.last_top_up_at` |
| Currency | KWD |

Output: CSV (downloadable from admin LiveView), scheduled via `wallet_reporting`

---

### Sprint 6 — Admin LiveViews (3 days)

All LiveViews follow the existing pattern: `use WalletWeb, :admin_live_view`, `on_mount {WalletWeb.AdminAuth, :ensure_authenticated_admin}`.

**New files in `apps/wallet_web/lib/wallet_web/live/admin/prepaid/`:**

| File | Route | Purpose |
|---|---|---|
| `programs_live.ex` | `/admin/prepaid/programs` | List all programs with stats; create new program |
| `program_show_live.ex` | `/admin/prepaid/programs/:id` | Program dashboard: stat cards + quick-action tiles |
| `cardholders_live.ex` | `/admin/prepaid/programs/:id/cardholders` | Table: name_en, name_ar, national_id, phone, card status, balance |
| `bulk_issuance_live.ex` | `/admin/prepaid/programs/:id/issuance` | CSV upload → preview table → confirm → progress; SLA countdown |
| `batch_top_up_live.ex` | `/admin/prepaid/programs/:id/top-up` | Upload statement CSV → preview → execute; 48h SLA badge |
| `balance_sweep_live.ex` | `/admin/prepaid/programs/:id/sweep` | Confirmation modal → execute sweep → summary |
| `merchants_live.ex` | `/admin/prepaid/programs/:id/merchants` | CRUD for allowed merchants; bilingual name fields |
| `monthly_report_live.ex` | `/admin/prepaid/programs/:id/report` | Date picker → generate → download CSV |

**Router additions (in `apps/wallet_web/lib/wallet_web/router.ex`):**

```elixir
scope "/admin/prepaid", WalletWeb.Live.Admin.Prepaid do
  pipe_through [:browser, :require_backoffice]

  live "/programs",                        ProgramsLive,      :index
  live "/programs/:id",                    ProgramShowLive,   :show
  live "/programs/:id/cardholders",        CardholdersLive,   :index
  live "/programs/:id/issuance",           BulkIssuanceLive,  :index
  live "/programs/:id/top-up",             BatchTopUpLive,    :index
  live "/programs/:id/sweep",              BalanceSweepLive,  :index
  live "/programs/:id/merchants",          MerchantsLive,     :index
  live "/programs/:id/report",             MonthlyReportLive, :index
end
```

**Admin Navigation** — Add "Prepaid Programs" entry to `WalletWeb.Navigation.AdminMenu` under a new **"Prepaid"** section.

---

### Sprint 7 — Customer PIN Change LiveView (0.5 day)

`WalletAuth.Pin.Commands.ChangePIN.execute/1` **already exists** (`apps/wallet_auth/lib/wallet_auth/pin/commands/change_pin.ex`). Only the UI is missing.

**New file:**

```
apps/wallet_web/lib/wallet_web/live/customer/prepaid/change_pin_live.ex
```

**Flow:**
1. Cardholder enters current PIN + new PIN + confirm PIN
2. `ChangePIN.execute(%{user_id, current_pin, new_pin})`
3. On `:invalid_current_pin` → show error flash
4. On success → confirmation message + PubSub notification

**Route addition:**

```elixir
live "/app/prepaid/change-pin", WalletWeb.Live.Customer.Prepaid.ChangePinLive, :index
```

---

### Sprint 8 — Closed-Loop POS Guard (1 day)

`WalletMerchant.Commands.AuthorizePosPayment` exists. The closed-loop check wraps it.

**New file:**

```
apps/wallet_prepaid/lib/wallet_prepaid/closed_loop_guard.ex
```

**Logic:**

```elixir
def before_pos_auth(card_id, merchant_id) do
  with {:ok, card} <- WalletCards.CardStore.get(card_id),
       true        <- card.is_prepaid,
       :allow      <- ClosedLoopPolicy.authorize_purchase(card.program_id, merchant_id) do
    :ok
  else
    false            -> :ok   # non-prepaid card, skip check
    {:deny, reason}  -> {:error, reason}
  end
end
```

This is called at the LiveView/API layer before `AuthorizePosPayment.execute/1`, avoiding modification of the compiled `wallet_merchant` source.

---

### Sprint 9 — Arabic Locale + RTL UI (1.5 days)

**Gettext is already configured.** Arabic locale files are missing.

**New files:**

```
apps/wallet_web/priv/gettext/ar/LC_MESSAGES/errors.po
apps/wallet_web/priv/gettext/ar/LC_MESSAGES/default.po
apps/wallet_web/priv/gettext/prepaid.pot
apps/wallet_web/priv/gettext/ar/LC_MESSAGES/prepaid.po
```

**Key translations in `prepaid.po`:**

| English | Arabic |
|---|---|
| Prepaid Programs | البرامج المدفوعة مسبقاً |
| Kuwait Benefits Card | بطاقة المزايا الكويتية |
| Cardholder Name | اسم حامل البطاقة |
| National ID | الرقم المدني |
| Balance | الرصيد |
| Top-Up Amount | مبلغ التعبئة |
| Card Status | حالة البطاقة |
| Active | نشط |
| Blocked | محجوب |
| Monthly Report | التقرير الشهري |
| Change PIN | تغيير رقم التعريف الشخصي |

**Locale Detection Plug (new):**

```
apps/wallet_web/lib/wallet_web/plugs/set_locale.ex
```

Reads `locale` from session; calls `Gettext.put_locale/2`. Added to `:browser` pipeline.

**RTL Layout change in `root.html.heex`:**

```heex
<html lang={Gettext.get_locale(WalletWeb.Gettext)}
      dir={if Gettext.get_locale(WalletWeb.Gettext) == "ar", do: "rtl", else: "ltr"}>
```

**RTL CSS in `assets/css/app.css`:**

```css
[dir="rtl"] {
  direction: rtl;
  text-align: right;
}
[dir="rtl"] .flex { flex-direction: row-reverse; }
[dir="rtl"] .pl-4 { padding-left: 0; padding-right: 1rem; }
```

Use Tailwind v3 `rtl:` variants (`rtl:text-right`, `rtl:pr-4`) throughout prepaid LiveViews.

---

### Sprint 10 — On-Premise Deployment Packaging (2 days)

**New files (infrastructure, not Elixir code):**

```
deploy/
  docker-compose.yml          — app + MySQL + Nginx + Redis (session store)
  docker-compose.prod.yml     — production overrides
  nginx/prepaid.conf           — reverse proxy, SSL termination
  .env.example                 — all required env vars documented
  Dockerfile                   — Elixir release build
  scripts/
    setup.sh                   — first-time server setup
    migrate.sh                 — run ecto.migrate
    backup_db.sh               — mysqldump + rotation
    health_check.sh            — validate all services running
```

**Environment Variables Required:**

| Variable | Purpose |
|---|---|
| `DATABASE_URL` | MySQL connection string |
| `SECRET_KEY_BASE` | Phoenix session encryption |
| `ENCRYPTION_KEY` | National ID field encryption |
| `PHX_HOST` | Domain for SSL cert |
| `SMTP_*` | Email notifications |
| `PROGRAM_NAME` | Brand name override |

---

## 7. Naming & Translation Strategy

### 7.1 Program Branding

The system currently hardcodes **"MomentPay"** in layout templates. For Kuwait, the brand will be configurable via `WalletDatabase.AdminConfig`:

| Config Key | Default | Kuwait Value |
|---|---|---|
| `program_name` | MomentPay | Kuwait Benefits Card |
| `program_name_ar` | — | بطاقة المزايا الكويتية |
| `sponsor_name` | — | Ministry of Social Affairs |
| `sponsor_name_ar` | — | وزارة الشؤون الاجتماعية |
| `program_logo_url` | /images/logo.svg | (custom URL) |
| `support_phone` | — | Kuwait local number |

A `WalletPrepaid.BrandingConfig` module reads from `AdminConfig` at runtime (cached 5 minutes), so the layout, PDF reports, and card statements all display the correct program name without redeployment.

### 7.2 Bilingual Fields

| Entity | New Fields |
|---|---|
| User / Cardholder | `name_en` (string), `name_ar` (string, RTL display), `national_id` (string, encrypted) |
| Prepaid Program | `name_en`, `name_ar`, `sponsor_name`, `sponsor_name_ar` |
| Allowed Merchant | `name_en`, `name_ar`, `location`, `location_ar` |

All Arabic fields are displayed with `dir="rtl"` HTML attribute in the UI and rendered right-to-left in PDF/CSV exports.

### 7.3 Translation Scope

The following UI surfaces will have full Arabic translations:

- Admin: Prepaid Programs section (all 8 LiveViews)
- Customer: Card dashboard, PIN change, transaction history
- Email notifications: Top-up confirmation, balance statement
- PDF/CSV reports: All column headers bilingual

Existing admin sections (KYC, AML, Settlement) will remain English-only in Phase 1.

---

## 8. Infrastructure Requirements (On-Premise)

### 8.1 Minimum Server Spec (5,000–6,000 cards)

| Component | Specification | Notes |
|---|---|---|
| **Application Server** | 4 vCPU, 8 GB RAM | Elixir/Phoenix BEAM; ETS is in-process |
| **Database Server** | 4 vCPU, 16 GB RAM, 500 GB SSD | MySQL 8.0+; InnoDB; transaction-heavy |
| **Reverse Proxy** | Nginx on same or separate VM | SSL termination; rate limiting |
| **OS** | Ubuntu 22.04 LTS / RHEL 8+ | Elixir 1.14+, Erlang/OTP 25+ |
| **Network** | 1 Gbps internal | App ↔ DB on private VLAN |

> For 6,000 cards with 20 transactions/day each: ~120,000 transactions/day = ~1.4 TPS average. A single BEAM node handles this with significant headroom. No clustering required for Phase 1.

### 8.2 High-Availability Option (Recommended)

| Component | HA Spec |
|---|---|
| App tier | 2× application servers behind Nginx load balancer |
| DB tier | MySQL Primary + 1 Read Replica; daily `mysqldump` backup |
| Session store | Redis (or cookie-based — already supported) |
| Failover | Manual promotion of replica; RTO ~15 min |

### 8.3 Software Stack

| Layer | Technology | Version |
|---|---|---|
| Runtime | Elixir / Erlang OTP | Elixir 1.14+, OTP 25+ |
| Web framework | Phoenix LiveView | 1.0+ |
| Database | MySQL | 8.0+ |
| Web server | Bandit (built-in) + Nginx | — |
| Container (optional) | Docker + Docker Compose | 24+ |
| Build | Elixir Mix Release | — |

### 8.4 Network & Security

- All traffic via HTTPS (TLS 1.2+); SSL certificate required (self-signed or CA-signed)
- Admin console restricted to client's internal IP range via Nginx `allow/deny`
- PIN stored as bcrypt hash (never plaintext, never logged)
- National ID encrypted with AES-256 deterministic cipher (existing `EncryptedDeterministic` type)
- Database accessible only from app server (firewall rule: block port 3306 from external)
- Daily automated backup + 30-day retention

### 8.5 Deployment Architecture Diagram

```
Internet / LAN
      │
      ▼
   Nginx (443/HTTPS)
   Rate limiting, IP allowlist for /admin/*
      │
      ├──▶ Phoenix App Server (port 4000)
      │        ETS (in-memory stores)
      │        BEAM cluster (single node, Phase 1)
      │              │
      │              ▼
      │         MySQL 8.0
      │         (prepaid_* tables + all wallet tables)
      │
      └──▶ Static Assets (priv/static via Nginx)
```

### 8.6 Monitoring & Operations

| Tool | Purpose |
|---|---|
| Phoenix LiveDashboard | Real-time BEAM metrics (already built-in) |
| `wallet_resilience` HealthCheck | Application-level health API |
| `wallet_observability` Telemetry | Custom metrics + audit events |
| `scripts/backup_db.sh` | Scheduled mysqldump |
| `scripts/health_check.sh` | Cron-based uptime check |

---

## 9. OPEX Pricing Model

### 9.1 Recommended Pricing Structure

| Model | Unit | Suggested Rate | Notes |
|---|---|---|---|
| **Card Maintenance** | Per active card / month | KWD 0.50–1.00 | Base fee for issuance, maintenance, support |
| **Transaction Fee** | Per purchase transaction | KWD 0.05–0.10 | Applied on each POS transaction |
| **Top-Up Processing** | Per batch | KWD 20–50 flat | Monthly batch regardless of card count |
| **Reporting** | Per monthly report run | Included | No extra charge for standard reports |
| **Onboarding** | One-time setup | KWD 2,000–5,000 | System deployment, data import, training |

**Example at 6,000 cards:**

| Item | Qty | Rate | Monthly |
|---|---|---|---|
| Card maintenance | 6,000 | KWD 0.75 | KWD 4,500 |
| Transactions (est. 10/card/month) | 60,000 | KWD 0.05 | KWD 3,000 |
| Top-Up batch | 1 | KWD 30 | KWD 30 |
| **Total monthly** | | | **KWD 7,530** |

> Adjust rates based on competitor proposals. The OPEX model aligns with client preference and avoids large upfront CAPEX.

### 9.2 What Is Included in OPEX

- System operation and maintenance
- Software updates and security patches
- Monthly batch top-up processing
- Real-time admin console access
- Standard reports (monthly balance, transaction history)
- Cardholder PIN self-service
- Email/SMS notifications (costs passed through at cost)
- Technical support (business hours)

### 9.3 What Is Billed Separately

- Physical card production (if required — vendor cost pass-through)
- SMS gateway costs per message
- Custom report development beyond standard templates
- Additional merchant onboarding beyond initial setup

---

## 10. Risk & Timeline Summary

### 10.1 Sprint Timeline

| Sprint | Scope | Est. Days | Status |
|---|---|---|---|
| S0 | Scaffold `wallet_prepaid` app | 0.5 | Pending |
| S1 | DB migrations (6 tables + 2 schema alterations) | 1.0 | Pending |
| S2 | PrepaidProgram domain: stores, commands, closed-loop policy | 1.5 | Pending |
| S3 | Bulk issuance: CSV parser + `BulkIssueCards` command | 2.0 | Pending |
| S4 | Batch top-up + balance sweep commands | 1.5 | Pending |
| S5 | Monthly balance report generation | 0.5 | Pending |
| S6 | Admin LiveViews (8 new views) | 3.0 | Pending |
| S7 | Customer PIN change LiveView | 0.5 | Pending |
| S8 | Closed-loop POS guard | 1.0 | Pending |
| S9 | Arabic locale + RTL CSS | 1.5 | Pending |
| S10 | On-premise deployment packaging | 2.0 | Pending |
| — | Testing + integration + UAT support | 3.0 | Pending |
| **Total** | | **~18–20 days** | |

### 10.2 Risk Register

| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Physical card provisioning required (not virtual) | Medium | High | Clarify with client. Physical cards need a card manufacturer integration (3rd party). Virtual cards work for most Kuwait cooperative POS systems. |
| Client's on-prem server spec is insufficient | Low | Medium | Provide minimum spec document upfront (Section 8.1). |
| Arabic font rendering in PDF reports | Medium | Low | Use a standard Arabic Unicode font (Noto Naskh Arabic). |
| National ID format varies by nationality | Low | Medium | Store as free-text string; client provides validation rules. |
| Client sends statements in non-CSV format (Excel, PDF) | Medium | Medium | Add XLSX parser (`xlsxir` library) in Sprint 3 if needed. |
| SLA breach (2-day top-up) due to client delay | Medium | Low | System records `statement_received_at`; SLA clock starts at upload, not client-send. |

### 10.3 What We Can Demo Today

The following is **live on the staging system** (`demo.ctrmv.com:24000`) without any changes:

- ✅ Admin card management console (`/admin/cards`) — full card search, block/unblock, stats
- ✅ Transaction inquiry (`/admin/transaction-inquiry`)
- ✅ User search with card details
- ✅ Audit log
- ✅ KYC case management
- ✅ Report generation (single account)
- ✅ Card freeze/unfreeze (customer-side)

These can be demonstrated to the Kuwait client as proof of core capability while the prepaid-specific features are built.

---

*Document prepared on the `prepaid-card-system` branch. For technical queries, refer to `docs/domain-map.md` and `docs/wallet-implementation-plan-apps-mode.md`.*
