# CloudLayer — Flow & API Reference

This document maps the actual, current behavior of the CloudLayer umbrella app: routes, request/data flow, and module responsibilities. It supersedes stale claims in `README.md` and `ARCHITECTURE.md` where they've drifted from the code (noted inline).

## 1. System Overview

CloudLayer is an Elixir/Phoenix **umbrella app** with one root app and two optional sibling apps compiled in only via env flags:

| App | Path | Purpose | Included when |
|---|---|---|---|
| `da_product_app` | `lib/` (root) | Main application: device management, MQTT, merchant/POS terminal management, user auth, transaction rules | always |
| `payment_gateway_app` | `apps/payment_gateway_app` | Payment gateway module — real-time checkout via MPGS (Mastercard Payment Gateway Services) and legacy YSP/Narada® | `INCLUDE_PAYMENT_GATEWAY=true` (compile-time) |
| `pg_settlement` | `apps/pg_settlement` | MPGS **Hosted Batch** settlement/reconciliation pipeline (Oban jobs, no web layer) | `INCLUDE_PG_SETTLEMENT=true` (compile-time) |

**Wiring**: `mix.exs` (root, L84-102) only adds `payment_gateway_app`/`pg_settlement` as path deps when the corresponding env var is `"true"` — if unset, those apps aren't even compiled. `da_product_app_web/router.ex` and `endpoint.ex` guard all references to the payment app with `Code.ensure_loaded?(PaymentGatewayAppWeb...)`, so the main app degrades gracefully when the module is absent. `application.ex` does **not** conditionally start the payment gateway as a child — it self-starts via normal OTP application startup once it's a compiled dependency.

Dependency direction is strictly one-way: `pg_settlement` → `payment_gateway_app` → (nothing); `da_product_app` is depended on by both (for `Repo` access) but never depends on them.

Two separate MySQL databases are in play: `DaProductApp.Repo` (primary) and `DaProductApp.Repos.ShukriaMmsRepo` (a second repo pointed at the `shukria_mms` database, holding merchant identity/`public_key`/branding — read by both `da_product_app` and `payment_gateway_app`).

---

## 2. `da_product_app` (main app)

### 2.1 Router (`lib/da_product_app_web/router.ex`)

**Pipelines**: `:browser` (session, CSRF, CSP, `fetch_current_user`) · `:api` (JSON, no auth) · `:api_auth` (JSON + `ApiKeyAuth` plug) · `:non_csrf` (session fetched, CSRF **skipped** — for external payment simulators/webhooks).

**Browser & LiveView:**

| Verb | Path | Handler | Auth |
|---|---|---|---|
| GET | `/` | `PageController.home` | none |
| GET/POST | `/order/confirmation` | `PageController.order_confirmation` | GET: browser · POST: non_csrf |
| GET/POST | `/payment/callback` | `PageController.payment_callback` | non_csrf |
| POST | `/transaction_post` | `TransactionPostController.new` | non_csrf (webhook) |
| LIVE | `/form`, `/live`, `/live/modal/:size`, `/live/slide_over/:origin`, `/live/pagination/:page` | demo LiveViews | browser |
| LIVE | `/dashboard`, `/sbomcomponent(/:origin)`, `/workflow`, `/software(/:id)` | Dashboard/SbomComponent/Workflow/Software LiveViews | **authenticated** |
| LIVE | `/users/register`, `/users/log_in`, `/users/reset_password(/:token)`, `/transactions/:id` | registration/login/reset LiveViews | redirect if already logged in |
| POST/DELETE | `/users/log_in`, `/users/log_out` | `UserSessionController` | browser |
| LIVE | `/users/settings(/confirm_email/:token)`, `/users/confirm(/:token)` | settings/confirmation LiveViews | authenticated / mount-only |
| GET/POST | `/dev/dashboard-system`, `/dev/mailbox` | LiveDashboard / Swoosh preview | authenticated, dev-only build flag |

**Public API** (`/api`, no auth) — device/QR bootstrap: `POST /device/initiate`, `POST /device`, `POST /Iotmsgtest/createQrMf`, `POST /qrmorefun/status`, `POST /device/status`.

**Protected API** (`/api`, `ApiKeyAuth` plug) — QR lifecycle (`/generate_qr`, `/qr`, `/qr/initiate`, `/qr/status`), Alipay notify, transaction processing (`/processTransaction`, `/processNewMiddleTransaction`, `/cancelPayment`, `/refundPayment`, `/reprint_last`), `GET /merchantTransactions`.

**Misc typed endpoints**: `POST /v1/transaction-rules/evaluate` → `TransactionRulesController.evaluate`; `GET /v1/merchant/logo/:user_id` → `MerchantLogoController.show`.

**General merchant API** (`/api`, **no auth despite sensitivity**) — large `MerchantApiController` surface: merchant hierarchy/brands/providers/store CRUD, chain/group CRUD, device save/update/force-update, duplicate TID/MID checks, Shukria terminal management, provider-alias lookups, MCC codes, refund details, admin transaction views; plus `TransactionsController`/`PosTransactionController` reads and `PaymentNotificationController.process_payment_success`.

**Payment-gateway mount** (only if `Code.ensure_loaded?(PaymentGatewayAppWeb.Router)`):
- `POST /pgpayments/process` → `PaymentGatewayAppWeb.PaymentMiddlewareController.process` (non_csrf)
- `GET|POST /pgpayments/callback` → `PaymentGatewayAppWeb.PageController.callback` (non_csrf)
- `forward "/pgpayments", PaymentGatewayAppWeb.Router` — mounts the entire sibling app's router (no CSRF enforced across this boundary)
- `GET /api/merchant/pgOnlineTransactions` → `PaymentGatewayAppWeb.PgOnlineTransactionsController.index`, deliberately declared in its own **unaliased** `scope "/api"` block so Phoenix doesn't mis-prepend the `DaProductAppWeb` alias onto a cross-app controller reference.

### 2.2 Contexts

- **`Users`** — registration, password/email change with token confirmation, session tokens, password reset. `User` (role enum `user/admin/superuser`). `UserToken`: 60-day session tokens, SHA-256-hashed email-delivered tokens. Emails sent via `UserNotifier` (plain-text Swoosh).
- **`Transactions`** — split across two modules of the same name: `transactions.ex` (raw-SQL reads against `pos_transaction`/`pos_terminal`, PAN masking) and `transactions/transactions.ex` (Ecto CRUD over `Transaction`). `Transaction` is a large flat schema mixing legacy fields with payment fields (`transaction_amount`, `provider_id`, `device_id`, `merchant_id`, `settlement_date_time`, `batch_number`, `ysp_tid`, JSON `additional_data`/`payload`). `TransactionOperation` generalizes refund/cancel/void/reversal; `TransactionRefund` is an older parallel refund-only schema.
- **`Software`** — CRUD over `SoftwareEntry`/`SoftwareVersion`. Note: `software/software.ex` defines a near-duplicate schema for the same table, with `list_software/0` still querying the other module — leftover refactor cruft, not actively broken but confusing.
- **`SBOM`** — CRUD over `Component` (SBOM component records), scoped by `user_id`/`application_id`/`organization_id`.
- **`MerchantConfiguration`** — schema (no context functions) over `merchant_configuration` table in the **Shukria MMS** database: logo, description, message, colors, settings — merchant checkout branding, read by `payment_gateway_app`.
- **`ShukriaMmsRepo`** — second Ecto repo (MyXQL) pointed at a separate database. Holds:
  - **`Schemas.ShukriaMms.User`** (table `users`): name/email/phone/role/status/kyc_status, **`public_key`/`secret_key`** (merchant API credentials — this is the "public key" referenced by the current branch), merchant_status, turnover/contract/fee JSON blobs, `has_many :user_metadata`/`:kyc_requests`.
  - **`Schemas.ShukriaMms.UserMetadata`** (table `user_metadata`): `merchant_id`, `merchant_reference_number`, currency/country/group/bank codes — `belongs_to :user`.
  - Together these are what `payment_gateway_app.MerchantAuth` queries to authenticate a widget request by `public_key` and resolve `merchant_reference_number`.
- Also present but not deep-dived: `device_middlelayer/*` (POS terminal/acquirer/ISO8583-adjacent domain), `qr_provider*` (Alipay/WeChat/Aani factory), `merchant_registration/*`, `merchant_static_qr/*`, `providers`/`brands`/`chains`/`stores`/`cashiers`/`groups`, `tcp/*` (Ranch scaffolding, currently not in the supervision tree).

### 2.3 MQTT / Device Flow

- **Bootstrap** (`lib/da_product_app/mqtt.ex`) — Tortoise MQTT client connects over TCP to `demo.ctrmv.com:1883`, subscribes to `/ack/qr-device/+` and `/ack/#` (QoS 1). Started as a `Task` in `application.ex` after `DaProductApp.MQTT.Supervisor`.
- **Handler** (`lib/da_product_app/mqtt/handler.ex`, `use Tortoise.Handler`):
  - `connection(:up/:down, state)` → `DeviceRegistry.track_online/offline(device_id)`.
  - `["", "ack", "qr-device", device_id]` messages → broadcasts `"ack_received"` on PubSub topic `"qr:ack:#{device_id}"` (no DB write).
  - `["", "ack", merchant_id, device_id]` messages → decodes JSON (`request_id`/`status`), looks up `CloudTransactions.CloudTransaction` by `transaction_ref_number`+`device_id`, updates `status`/`acknoledgment`, broadcasts the same PubSub event — this is how async device acknowledgments propagate to subscribed LiveViews.
- **Device registry** (`lib/da_product_app/device_registry.ex`) — in-memory `GenServer`. **Note:** `is_online?/1` is currently hardcoded to always return `true`; the real map-lookup is commented out.

### 2.4 Auth Flow (`lib/da_product_app_web/user_auth.ex`)

Standard `phx.gen.auth`-style session-cookie auth (DB-backed session tokens, not JWT).
- `log_in_user/3` — generates session token, renews session (anti-fixation), optionally sets a 60-day signed remember-me cookie, redirects to `user_return_to` or `/dashboard`.
- `log_out_user/1` — deletes the DB session token, broadcasts `"disconnect"` on the `live_socket_id` PubSub topic (kills live sockets), clears remember-me cookie.
- `fetch_current_user/2` plug — reads token from session or remember-me cookie, loads user, assigns `:current_user`.
- LiveView `on_mount`: `:mount_current_user`, `:ensure_authenticated` (redirects to login), `:redirect_if_user_is_authenticated`.
- **Note:** two identical `mount_current_user/3` clauses exist (dead duplicate).

### 2.5 Emails

- `UserNotifier` (plain-text, Swoosh) sends the only emails actually triggered today: confirmation instructions, password reset, email-change confirmation.
- `lib/da_product_app_web/emails.ex` + `lib/da_product_app/mailer.ex` (HTML templating via Premailex, `invite_user.html.heex` template) is unused scaffolding — no caller found.

---

## 3. `payment_gateway_app`

This is the real, working payment integration. MPGS is the only fully implemented provider; a second "generic provider" abstraction exists but is disconnected demo scaffolding (see §3.4).

### 3.1 Module Inventory

**Core (`lib/payment_gateway_app/`)**
| Module | Purpose |
|---|---|
| `payment_gateway_app.ex` | Public facade (`start_payment/4`, `get_payment_status/1`) — delegates to the mock provider stack |
| `application.ex` | Starts Telemetry, PubSub, Finch, Endpoint |
| `api_adapter.ex` | Generic provider-agnostic adapter, dispatches to configured `:api_provider` |
| `payment_initiator.ex` / `status_checker.ex` / `validator.ex` | Generic-adapter pipeline: build → call → validate (currency allowlist USD/EUR/GBP/JPY/CNY/INR only — no AED) |
| `providers/payment_provider.ex` | Behaviour: `create_payment/1`, `get_payment_status/1`, `cancel_payment/1` |
| `providers/mock_provider.ex` | Fake provider, random statuses — the default and prod fallback |
| `merchant_auth.ex` | Authenticates a widget request by `public_key` → `user_id` (Shukria MMS lookup) |
| `merchant_route.ex` | Schema `merchant_payment_routing` (gateway ∈ `ysp`/`mpgs`) |
| `routing.ex` | Picks `:ysp` vs `:mpgs` per merchant; fails safe to `:ysp` |
| `transaction_helper.ex` | Builds `pg_transactions` attrs from YSP/Narada payloads |
| `transactions.ex` | Ecto context for `pg_transactions` (shared table for both gateways) |
| `pg_transactions/pg_transaction.ex` | Schema for the shared `pg_transactions` table |
| `payment_result.ex` | Gateway-neutral result envelope (status codes 1200/1400/1100) returned to merchants |
| `ysp_checksum.ex` | HMAC/checksum for the legacy YSP/Narada® gateway |

**MPGS subtree (`lib/payment_gateway_app/mpgs/`)**
| Module | Purpose |
|---|---|
| `mpgs.ex` | Entry point: `verify_credentials/1`, `payment_options_inquiry/1`, `retrieve_order/2`, `retrieve_transaction/3`, `webhook_url/0` |
| `client.ex` | HTTP transport (Req), Basic auth, error normalization, redacts card/CVV in sandbox logs |
| `credentials.ex` | Resolves/decrypts a merchant's MPGS config (`Credentials.resolve(user_id)` → `Config` struct); region→host mapping; capability predicates |
| `merchant_credential.ex` | Schema `mpgs_merchant_credentials` (encrypted secrets, method/interaction/3DS/region, `direct_approved`, `batch_enabled`) |
| `vault.ex` | AES-256-GCM encrypt/decrypt, key from `MPGS_CREDENTIAL_KEY` |
| `checkout_builder.ex` | Shared payload builders (order/interaction/customer/billing, amount formatting, ISO2→ISO3 country) |
| `checkout.ex` | Hosted Checkout: `initiate/2`, `script_url/1` (`checkout.min.js`) |
| `session.ex` | Hosted Session: `create/1`, `update/3`, legacy 3DS1, `script_url/1` (`session.js`) |
| `authentication.ex` | 3DS2: `initiate/2`, `authenticate_payer/2`, `proceed?/1` |
| `transaction.ex` | `pay/2` (PAY/AUTHORIZE), `capture/5`, `refund/5`, `void/3` |
| `order_sequence.ex` | Atomic per-merchant order-ref counter (MySQL `LAST_INSERT_ID` idiom) |
| `mapper.ex` | Classifies MPGS responses into `:success/:failed/:pending`, maps to `pg_transactions` columns |
| `payments.ex` | Orchestration layer tying DB + gateway together (812 lines — largest MPGS module) |
| `webhook.ex` | Verifies/processes async MPGS notifications |
| `batch.ex` | Manual acquirer settlement-batch closure (distinct from `pg_settlement`'s Hosted Batch pipeline) |
| `direct_api_key.ex` | Server-to-server API keys for Direct (raw-card) integration |

**Web layer (`lib/payment_gateway_app_web/`)**
| Module | Purpose |
|---|---|
| `router.ex` | see §3.2 |
| `controllers/page_controller.ex` | Widget front door (`iframe/2`), YSP sandbox/gateway pages, checksum endpoint |
| `controllers/mastercard_controller.ex` | All MPGS payer-facing endpoints (1320 lines) |
| `controllers/direct_controller.ex` | Server-to-server Direct API (raw card, Bearer auth) |
| `controllers/payment_middleware_controller.ex` | Legacy YSP/Narada NAR form middleware |
| `controllers/pg_online_transactions_controller.ex` | Read-only merchant-portal transaction listing (mounted from main router, not this app's own) |
| `controllers/payment_controller.ex` | `/api/initiate`, `/api/status/:id` — generic/mock stack only |
| `plugs/direct_api_auth_plug.ex` | Bearer-token auth for Direct API |
| `live/payment_live/{gateway,sandbox}.ex` | Real YSP/Narada® card-entry LiveViews |
| `live/payment_live/{initiate,status}.ex` | Demo LiveViews wired to the mock provider |

**Mix tasks**: `mpgs.onboard_merchant.ex` (creates/updates `mpgs_merchant_credentials`, encrypts secrets via Vault), `mpgs.close_batch.ex` (CLI wrapper for `Mpgs.Batch.close/3`).

### 3.2 Router

Mounted at `/pgpayments` via `forward` from the main router. Paths below are relative to that prefix.

| Verb | Path | Handler |
|---|---|---|
| GET/POST | `/` | `PageController.iframe` — decodes widget token, routes to `/pay/start` (MPGS) or `/sandbox`/`/gateway` (YSP) |
| GET | `/shukria-payment-widget.js` | `PageController.widget` |
| GET | `/demo` | `PageController.demo` |
| GET | `/sandbox`, `/gateway` | plain YSP payment pages |
| POST | `/process` | `PaymentMiddlewareController.process` (NAR forward) |
| POST | `/prepare-redirect` | `PageController.prepare_redirect` |
| GET/POST | `/iframe`; POST `/callback` | browser pipeline |
| LIVE | `/live_initiate`, `/status/:payment_id` | mock-provider demo LiveViews |
| GET/POST | `/pay/*` and `/mastercard/*` (identical, `/mastercard` retained as alias) | `start`, `checkout`, `checkout-continue`, `session`, `submit`→`pay`, `3ds-return`, `return`, `cancel`, `callback` (webhook), `status/:id`, `complete`→`pay_complete`, `3ds-complete`→`three_ds_complete` |
| POST | `/api/initiate`; GET `/api/status/:payment_id` | `PaymentController` — **mock provider, not MPGS** |
| POST | `/api/checksum` | `PageController.checksum` (YSP/NAR HMAC) |
| POST | `/api/direct/pay` | `DirectController.pay` — Bearer-auth pipeline |

> **README.md is stale**: it documents `GET /pgpayments`, `/pgpayments/initiate`, `/pgpayments/status/:id`, `POST /api/pgpayments/initiate`, `GET /api/pgpayments/status/:id`. None of these exist. Real paths are `/pgpayments/pay/*`, `/pgpayments/mastercard/*`, `/pgpayments/api/initiate`, `/pgpayments/api/status/:payment_id`. `MPGS_INTEGRATION.md`'s route table is also incomplete (omits `/pay/*` aliases, `/api/direct/pay`, `/api/merchant/pgOnlineTransactions`).

### 3.3 MPGS Integration — Public Key / Widget Mapping

This is the area the current branch (`fix/mpgs-widget-public-key-mapping`) targets. Two unrelated "keys" exist and are easy to conflate:

1. **Shukria/merchant `public_key`** (`shukria_mms.users.public_key`) — used only for **authentication/routing**, never sent to MPGS. `MerchantAuth.authenticate/1` (`merchant_auth.ex:38-46`) looks up `users` by `public_key`, returns the row's `id` as the trusted `user_id`; `secret_key` is not checked at this step. `MastercardController.authenticate/1` calls this, then `MerchantAuth.merchant_id_for/1` (resolves `user_metadata.merchant_reference_number`) and `Routing.mpgs?/1`. The resulting `user_id` — never anything caller-supplied — is what every downstream MPGS credential lookup uses (`with_authenticated_id/4`, `mastercard_controller.ex:50-54`).
2. **MPGS's own `merchant_id`/host/session config** — stored in `mpgs_merchant_credentials`, resolved by `Credentials.resolve(user_id)` (`credentials.ex:139-166`) into a `Config` struct keyed by the **authenticated** `user_id`, never by the shukria `public_key` directly.

The hosted-session **widget script URL** has no separate "public key" concept at all — `Session.script_url/1` (`mpgs/session.ex:148-151`) builds `"#{scheme}://#{host}/form/version/#{version}/merchant/#{merchant_id}/session.js"` purely from the resolved `Config` (`host`/`merchant_id`/`api_version`). `Checkout.script_url/1` similarly builds `checkout.min.js` from `config.host`/`config.scheme` only.

`public_key` is carried through payment params solely so it can be stored as `pg_transactions.s_mid` for reconciliation (`payments.ex:685-689`) — explicitly noted in `pg_transaction.ex:10` as distinct from MPGS's own `merchant_id`.

**Likely bug class given the branch name**: a mismatch between which `user_id`/`merchant_id` is used to build the `session.js`/`checkout.min.js` URL versus which was used at the `MerchantAuth` authentication step — i.e. an error in the mapping between the shukria `public_key → user_id` step and the `Credentials.resolve/1` step, not an issue with any MPGS-side "public key."

**Merchant onboarding**: `mix mpgs.onboard_merchant` creates/updates one `mpgs_merchant_credentials` row per `--user-id` (the same id `MerchantAuth` authenticates against), encrypting secrets via `Vault.encrypt/1`.

**Batch operations**: `mix mpgs.close_batch` forces early closure of the acquirer's daily settlement batch via a single `PUT batch` call — **not** the Hosted Batch CSV pipeline (that's `pg_settlement`, §4).

### 3.4 Payment Flow, End-to-End

**Real (MPGS) flow:**
1. `PageController.iframe` decodes the widget's base64 token → `Routing.gateway_for/1` → `:mpgs` → redirect to `/pay/start`.
2. `MastercardController.start/2` authenticates (`MerchantAuth` + `Routing.mpgs?`), resolves `Credentials.Config`, branches on `Config.hosted_session?/1`.
3. `Mpgs.Payments.start_hosted_checkout/2` or `start_hosted_session/2` — allocates an `OrderSequence` ref, `record_pending/3` into `pg_transactions`, creates the gateway `Session`/`Checkout`, persists `gateway_session_id`.
4. **Hosted checkout** return path (`/pay/return`) → `Payments.complete_hosted_checkout/2`, which **re-retrieves the order from MPGS** (never trusts the return URL's `resultIndicator` alone), settles via `Mapper.from_order/1`.
5. **Hosted session** path: `/pay/submit` → `/pay/complete` → `Payments.submit_hosted_session/2`, optionally 3DS2 (`Authentication`) or legacy 3DS1 (`Session`), then `Transaction.pay/2`.
6. Async settlement-of-record: `Webhook.process/2`, authenticated by a per-merchant `X-Notification-Secret`.
7. `PaymentResult.from_transaction/2` produces the gateway-neutral response envelope regardless of YSP or MPGS.

**Generic-adapter pipeline** (`PaymentInitiator` → `Validator` → `APIAdapter` → `PaymentProvider` behaviour, matching `ARCHITECTURE.md`'s diagram) is real code but is a **disconnected, aspirational layer**: only reachable via `/api/initiate`, `/api/status/:payment_id`, and the `/live_initiate`/`/status/:payment_id` LiveViews. Its only implementation is `MockProvider` (random status, fake IDs) — `config/prod.exs` falls back to it even if `PG_PROVIDER_MODULE` is unset/invalid. **MPGS is the only real, working provider**, implemented entirely in `mpgs/*` + `MastercardController`/`DirectController`, on a completely separate code path.

### 3.5 Data Model

All migrations live in the shared `priv/repo/migrations/` (via `DaProductApp.Repo`):
- `pg_transactions` — shared by both YSP and MPGS rows (`gateway` field discriminates).
- `mpgs_merchant_credentials` — encrypted secrets, method/interaction/3DS/region, `batch_enabled`, `direct_approved`.
- `mpgs_order_sequences` — backing store for `OrderSequence`.
- `merchant_payment_routing` — YSP vs MPGS routing decision.
- `mpgs_direct_api_keys` — Direct API server-to-server keys.

No `merchants` table lives here — merchant identity/branding/`public_key` live in the Shukria MMS database, queried via `DaProductApp.Repos.ShukriaMmsRepo` (§2.2).

---

## 4. `pg_settlement`

Pure OTP/library app — **no Phoenix router, no web layer**. Implements MPGS's **Hosted Batch** API (CSV file upload/status/response), distinct from `payment_gateway_app`'s real-time REST checkout API.

### 4.1 Module Inventory

| Module | Purpose |
|---|---|
| `application.ex` | Empty supervisor — app contributes only Oban workers |
| `mix.tasks.pg_settlement.run` | Manual CLI entry (mover/build/poll/response phases; `--force` for destructive ones) |
| `mover.ex` | Sweeps terminal `pg_transactions` rows into `pg_settlement_transactions` |
| `batch_builder.ex` | Builds & uploads CAPTURE batch CSVs for `batch_enabled` merchants |
| `batch_name.ex` | Allocates unique batch names via atomic counter (`mpgs_order_sequences`) |
| `batch.ex` | Schema: one row per Hosted Batch upload, gateway + local status |
| `settlement_transaction.ex` | Schema: archived/settlement-tracked transaction |
| `client.ex` | HTTP transport for the 4 Hosted Batch REST endpoints (upload/validate/status/response) |
| `credentials.ex` | Wraps `PaymentGatewayApp.Mpgs.Credentials` for batch-specific URL/auth (empty-username Basic Auth) |
| `csv.ex` / `csv_parser.ex` | Encodes/parses MPGS "Native Format" CSV |
| `mic.ex` | SHA-1 "Message Integrity Code" of uploaded bytes |
| `response_processor.ex` | Downloads completed batch's response CSV, matches rows by `order.id`+`transaction.id`, reconciles amount, updates status |
| `status_checker.ex` | Polls status endpoint for outstanding batches, enqueues response processing on completion |
| `workers/*` | Oban jobs: `MoverWorker` (2 min), `BatchBuilderWorker` (15 min), `StatusPollerWorker` (2 min), `ResponseProcessorWorker` (enqueued by `StatusChecker`) |

### 4.2 Flow

Four phases, all against MPGS Hosted Batch (not the real-time API):
1. **Mover** — sweeps `pg_transactions` with `closure_status IN ('CLOSED','FAILED')`, classifies PAY vs AUTHORIZE from `raw_payload` (closure_status alone can't distinguish captured payment from awaiting-capture auth). PAY/CAPTURE → `settled`; AUTHORIZE → `awaiting_capture`; FAILED → `failed`.
2. **BatchBuilder** — for `batch_enabled` merchants, gathers `awaiting_capture` rows, dedupes by order (MPGS forbids mixing ops on one order per batch), chunks under 18,000 records/2.9MB, builds CAPTURE CSV, computes MIC, uploads then validates via `Client`, records a `Batch` row.
3. **StatusChecker** — polls status for non-terminal batches; on `complete`, enqueues `ResponseProcessorWorker`.
4. **ResponseProcessor** — downloads response CSV, matches by `order.id`+`transaction.id` (not position), reconciles submitted vs. returned amount, sets `settled`/`capture_failed`.

### 4.3 Coupling & Data Model

- No reverse dependency: `payment_gateway_app` never calls into `pg_settlement` (only comment references explaining why `MerchantCredential.batch_enabled` exists).
- Migrations for `pg_settlement_transactions` / `pg_settlement_batches` live in the shared `da_product_app` repo. Also touches `pg_transactions` (owned by `payment_gateway_app`) and reuses `mpgs_order_sequences`.
- Runs on its own named Oban instance/queue (`PgSettlement.Oban` / `settlement`) to avoid colliding with the primary Oban instance sharing the same database.

---

## 5. Cross-App Integration Summary

```
Widget (merchant site)
   │  public_key + payload
   ▼
da_product_app router ──forward──▶ payment_gateway_app router (/pgpayments/*)
   │                                    │
   │                                    ├─ MerchantAuth: public_key → user_id  (queries ShukriaMmsRepo.User)
   │                                    ├─ Credentials.resolve(user_id) → MPGS Config (queries mpgs_merchant_credentials)
   │                                    ├─ Mpgs.Payments → Mpgs.{Session,Checkout,Transaction,Webhook} → MPGS API
   │                                    └─ pg_transactions (shared table)
   │                                            │
   ▼                                            ▼
DaProductApp.Repos.ShukriaMmsRepo         pg_settlement (Oban, offline)
(public_key, merchant branding)           sweeps closed pg_transactions →
                                           Hosted Batch upload/status/response → MPGS
```

Key shared state: `pg_transactions` (written by `payment_gateway_app`, read by `pg_settlement`), `mpgs_order_sequences` (written by both), `ShukriaMmsRepo.User`/`UserMetadata` (merchant identity, read-only from `payment_gateway_app`).

---

## 6. Known Gaps / Stale Docs

- `README.md` payment-gateway route table is out of date (see §3.2).
- `ARCHITECTURE.md`'s Stripe/PayPal provider mentions are aspirational — only `MockProvider` and MPGS exist.
- `da_product_app/lib/da_product_app/software/software.ex` is a near-duplicate, partially-broken schema module for the same table as `Software.SoftwareEntry`.
- `DeviceRegistry.is_online?/1` is hardcoded to `true`; real tracking logic is commented out.
- `user_auth.ex` has a duplicate `mount_current_user/3` clause.
- HTML email scaffolding (`emails.ex`, `mailer.ex`, `invite_user.html.heex`) is unused — all live email sends go through `UserNotifier` in plain text.
- The main app's "General merchant API" (`/api/*` under `MerchantApiController` etc.) has no auth pipeline despite covering merchant/store/device CRUD — worth flagging if not intentional.
