# Tap (PayTap) Integration Tasks — Suqna

> Gateway: **Tap Payments** (not PayTabs).


> **Reference only:** `../swapo-reference` — do **not** modify it.  
> **Implement here:** `suqna`  
> Status legend: `[ ]` pending · `[~]` in progress · `[x]` done

---

## 0. Goal

Replace Suqna’s **simulated** online payment (`OnlinePayment` always succeeds) with a real **Tap hosted-page** flow, following the same pattern used in Swapo:

1. Create pending payment / payable record  
2. Call Tap `/v2/charges` → get `tran_ref` + `redirect_url`  
3. Client opens hosted page  
4. Return via redirect relay → frontend calls **verify**  
5. Server webhook also confirms via Tap `/v2/charges/{id}` (`status in CAPTURED|AUTHORIZED`)  
6. Only then mark payment `paid` and activate order/subscription side-effects  

Wallet and COD stay local (no Tap).

---

## 1. Reference: How Tap works in Swapo (`swapo-reference`)

### 1.1 Architecture

| Piece | Path (reference) | Role |
|-------|------------------|------|
| Contract | `app/Contracts/PaymentGatewayInterface.php` | Shared gateway API |
| Factory | `app/Services/PaymentGatewayFactory.php` | Resolves Tap (or Tap) from settings |
| Client | `app/Services/TapService.php` | Create / query / brand mapping / payloads |
| HTTP | `app/Http/Controllers/Api/App/Payment/PaymentController.php` | `webhook`, `redirect`, `verify` |
| Order biz | `app/Services/Api/App/Order/OrderPaymentService.php` | Init + finalize orders |
| Promo biz | `app/Services/Api/App/ProductPromotion/PromotionPaymentService.php` | Init + finalize promotions |
| Config | `config/services.php` → `tap` | Env-driven credentials |

### 1.2 Env / config (copy into Suqna)

```
TAP_PROFILE_ID=
TAP_SERVER_KEY=
TAP_BASE_URL=https://secure.tap.com
TAP_FRONTEND_RETURN_URL=
```

Mapped in reference as:

```php
'tap' => [
    'profile_id'          => env('TAP_PROFILE_ID'),
    'server_key'          => env('TAP_SERVER_KEY'),
    'base_url'            => env('TAP_BASE_URL', 'https://secure.tap.com'),
    'frontend_return_url' => env('TAP_FRONTEND_RETURN_URL'),
],
```

### 1.3 Tap API usage (reference)

| Action | Endpoint | Notes |
|--------|----------|--------|
| Create payment | `POST {base}/v2/charges` | Header `authorization: SERVER_KEY`; returns `tran_ref`, `redirect_url` |
| Verify / query | `POST {base}/v2/charges/{id}` | Body: `profile_id`, `tran_ref` |
| Approved? | `payment_result.status in CAPTURED|AUTHORIZED` | Also amount-match within `0.01` |
| Brand filter | `payment_methods` array | `card`→`[creditcard,mada]`, `mada`, `applepay`, `stcpay` |

**Webhook security in reference:** no HMAC; trust = re-query Tap with server key.

### 1.4 Reference routes (prefix `/api/client`)

| Method | Path | Purpose |
|--------|------|---------|
| `POST` | `/payment/webhook` | Tap server callback (public) |
| `GET\|POST` | `/payment/redirect` | Relay user back to frontend + `tran_ref` (public) |
| `POST` | `/payment/verify` | Frontend confirms payment after return (auth) |
| `POST` | `/buyer-order/{id}/initialize-payment` | Start order online payment |
| `GET` | `/buyer-order/{id}/payment-status` | Poll status |
| `POST` | `.../promotion/initialize-payment` | Promo boost payment |
| `POST` | `/subscriptions/{id}/initialize-payment` | Renewal payment |

### 1.5 Reference flow (happy path)

```
Client selects online (+ optional brand)
  → initialize-payment
  → TapService::createPayment
  → store transaction_id / payment_gateway
  → return { tran_ref, payment_url }
  → User pays on Tap
  → return URL → /payment/redirect → frontend?tran_ref=...
  → Client POST /payment/verify { payment_reference }
  → (parallel) Tap POST /payment/webhook
  → query Tap → if A + amount OK → finalize payable
```

Wallet path never hits Tap. BNPL (Tabby/Tamara) in Swapo forces Tap — **out of scope for Suqna Tap v1** unless product asks for Tap later.

### 1.6 Docs in reference (read when implementing)

- `PAYMENT_FLOW_DIAGRAM.md`
- `PAYMENT_BRAND_HANDLING.md`
- `BRAND_SUPPORT_MATRIX.md`
- `tests/Feature/TapIntegrationTest.php` (paths may be stale; logic still useful)

---

## 2. Suqna today — payment touchpoints (analysis)

### 2.1 Current pattern (already in place)

```
PaymentFactory::make(method)
  → WalletPayment | CodPayment | OnlinePayment
  → PaymentContext::pay(Payment)
       → preCheck → process → Payment::onPaymentSuccess / onPaymentFailure
```

| File | Role today |
|------|------------|
| `app/Context/Payment/PaymentFactory.php` | Maps `wallet` / `cod` / `online` |
| `app/Context/Payment/PaymentContext.php` | Orchestrates pay + logging |
| `app/Strategy/Payment/OnlinePayment.php` | **Stub — always succeeds** |
| `app/Strategy/Payment/WalletPayment.php` | Real wallet debit |
| `app/Strategy/Payment/CodPayment.php` | Local COD |
| `app/Models/Payment.php` | Morph to `OrderGroup` / `Subscription`; success/fail hooks |
| `app/DTO/Order/PaymentData.php` | Builds payment row; **ONLINE is pre-set to `PAID`** |
| `app/Enums/PaymentMethod.php` | `online`, `cod`, `wallet` |
| `app/Enums/PaymentStatus.php` | `paid`, `not_paid`, `failed`, `partially_paid` |

`payments` table already has useful columns: `transaction_code`, `metadata`, `payment_status`, morph `payable`.

### 2.2 Flows that charge money (integration targets)

| Flow | Route | Controller / service | Notes |
|------|-------|----------------------|-------|
| **Order checkout** | `POST /api/app/client/orders` | `OrderController` → `OrderService::createOrder` | Sync DB transaction; online marked paid before gateway |
| **Package subscribe** | `POST /api/app/client/package/subscribe` | `PackageController::subscribe` | Payment created as `PAID` then `PaymentFactory::pay` |
| Wallet pay | same routes with `wallet` | `WalletPayment` | Keep as-is |
| COD pay | order only with `cod` | `CodPayment` | Keep as-is |

### 2.3 Related but not Tap (unless product requires later)

| Area | Routes | Why |
|------|--------|-----|
| Admin payments list | `GET /api/dashboard/admin/payments` | Read-only reporting |
| Cancel / refund | `PATCH .../orders/{id}/cancel` + order states | Wallet refund only — **no gateway refund** |
| Returns | `apiResource returns` | Wallet refund |
| Client wallet | `GET /api/app/client/wallet` | Read-only; **no top-up** |
| Vendor wallet / withdrawals | vendor dashboard | Payout, not checkout |

### 2.4 Critical blockers for hosted Tap

1. **`OnlinePayment` is synchronous and always succeeds** — must initiate gateway + return URL, not call `onPaymentSuccess()` immediately.  
2. **`PaymentData::create`** sets ONLINE → `PaymentStatus::PAID` before any gateway call.  
3. **`PackageController::subscribe`** hardcodes `'payment_status' => PaymentStatus::PAID`.  
4. **`OrderService::createOrder`** runs create + pay + clear cart inside one transaction and fails the whole order if `pay()` is false — incompatible with “pending → redirect → later confirm”.  
5. **No** callback / webhook / verify routes exist.  
6. **No** Tap config in Suqna `config/services.php` / `.env`.

---

## 3. Target architecture in Suqna

Adapt Swapo’s gateway layer into Suqna’s existing Strategy pattern:

```
POST orders / package/subscribe (payment_method=online)
  → create OrderGroup/Subscription + Payment (status: NOT_PAID / pending)
  → OnlinePayment::processPayment
       → TapService::build*Payload + createPayment
       → store tran_ref on Payment.transaction_code (+ metadata)
       → DO NOT call onPaymentSuccess yet
       → expose payment_url / tran_ref to API response
  → Client opens Tap page
  → GET|POST /api/app/client/payment/redirect  (public relay)
  → POST /api/app/client/payment/verify        (auth)
  → POST /api/app/client/payment/webhook       (public)
       → TapService::verifyPayment
       → if approved + amount match → Payment::onPaymentSuccess()
```

Wallet / COD: unchanged strategy paths.

---

## 4. Implementation tasks

### Phase A — Foundation (config + gateway client)

- [x] **A1.** Add Tap env keys to `.env.example`
- [x] **A2.** Add `tap` array to `config/services.php`
- [x] **A3.** Create `app/Contracts/PaymentGatewayInterface.php`
- [x] **A4.** Create `app/Services/TapService.php`
- [x] **A5.** Direct `TapService` in `OnlinePayment` (no multi-gateway factory for v1)

### Phase B — Data model & pending status

- [x] **B1.** `PaymentData::create` — ONLINE starts as `NOT_PAID`, `paid_amount = 0`
- [x] **B2.** `PackageController::subscribe` — online payment `NOT_PAID`
- [x] **B3.** `transaction_code` = tran_ref; metadata holds payment_url, cart_id, brand, gateway
- [x] **B4.** Skipped dedicated column — gateway stored in `metadata.gateway` + `transaction_type`
- [x] **B5.** cart_id: `og-{id}-{ts}` / `sub-{id}-{ts}`
- [x] **B6.** Orders stay pending while unpaid; cart cleared after successful Tap init (same as sync checkout)

### Phase C — Strategy & checkout/subscribe async flow

- [x] **C1.** `OnlinePayment` initiates Tap; does not call `onPaymentSuccess`
- [x] **C2.** URL/tran_ref stored on Payment; controllers return `PaymentResource`
- [x] **C3.** `OrderService::createOrder` online path + response with payment_url
- [x] **C4.** `PackageController::subscribe` online path
- [x] **C5.** Wallet/COD unchanged
- [x] **C6.** `brand`, `return_url` on CreateOrderRequest + SubscribeRequest

### Phase D — Callback / redirect / verify routes

- [x] **D1.** `PaymentController` — webhook / redirect / verify
- [x] **D2.** Routes registered in `routes/api/app/client.php`
- [x] **D3.** Webhook/redirect public (no auth)
- [x] **D4.** API routes (no CSRF issue)
- [x] **D5.** Idempotent finalize via `PaymentFinalizationService`
- [x] **D6.** Amount tolerance `0.01`
- [x] **D7.** `Payment::onPaymentSuccess` activates subscription
- [x] **D8.** Cart/coupon cleared at order create after successful init (not deferred)

### Phase E — Brand / UX

- [x] **E1.** Brands: card, mada, applepay, stcpay
- [x] **E2.** Validation rejects unsupported brands
- [x] **E3.** Tap/BNPL out of scope

### Phase F — Cancel / refund policy (post-payment)

- [ ] **F1.** Document: cancels/returns today refund to **wallet** only.
- [ ] **F2.** (Optional later) Tap refund API for online-paid cancellations — **not required for v1** unless product demands card refund.
- [ ] **F3.** Pending unpaid online orders: define expiry (job) → fail payment + restore stock/cart policy.

### Phase G — Admin / observability

- [ ] **G1.** Admin `PaymentResource` / filters: show `transaction_code`, gateway, pending vs paid.
- [ ] **G2.** Logging: create/query/webhook/verify (no secrets in logs).
- [ ] **G3.** Settings: if Suqna uses `setting('online')` already for enabling method — keep; optional `payment_gateway` setting only if multi-gateway later.

### Phase H — Tests & QA

- [ ] **H1.** Feature test: initialize order online → Http::fake Tap create → response has `payment_url` + pending status.
- [ ] **H2.** Feature test: webhook/verify approved → payment `paid`; subscription `active`.
- [ ] **H3.** Feature test: amount mismatch / non-`A` status → stays unpaid.
- [ ] **H4.** Feature test: wallet + COD unchanged.
- [ ] **H5.** Manual QA checklist with Tap sandbox profile.

---

## 5. Suggested file checklist (create / touch in Suqna only)

### Create

| File | Purpose |
|------|---------|
| `app/Contracts/PaymentGatewayInterface.php` | Gateway contract |
| `app/Services/TapService.php` | Tap API client |
| `app/Http/Controllers/Api/App/Payment/PaymentController.php` | webhook / redirect / verify |
| `app/Http/Requests/Api/App/Payment/VerifyPaymentRequest.php` | verify validation |
| `database/migrations/xxxx_add_payment_gateway_to_payments_table.php` | optional column |
| `tests/Feature/TapPaymentTest.php` | coverage |

### Modify

| File | Change |
|------|--------|
| `config/services.php` | `tap` config |
| `.env.example` | Tap env vars |
| `app/Strategy/Payment/OnlinePayment.php` | Real initiate (no instant success) |
| `app/Context/Payment/PaymentContext.php` | Support pending/online URL return if needed |
| `app/DTO/Order/PaymentData.php` | ONLINE → `NOT_PAID` |
| `app/Services/General/OrderService.php` | Async online checkout |
| `app/Http/Controllers/Api/App/Package/PackageController.php` | Async online subscribe |
| `app/Models/Payment.php` | Helpers to store/read gateway metadata; finalize idempotency |
| `routes/api/app/client.php` | payment routes |
| Order/subscribe FormRequests | `brand`, `return_url` optional |

### Do not touch

- Anything under `../swapo-reference`

---

## 6. Out of scope (v1)

- [ ] Tap / Tabby / Tamara BNPL  
- [ ] Wallet top-up via Tap  
- [ ] Tap refund / void API  
- [ ] Changing Swapo reference code  
- [ ] Vendor payout gateway  

---

## 7. Acceptance criteria

1. Online order: client receives Tap `payment_url`; payment stays `not_paid` until verify/webhook approval.  
2. Online subscribe: subscription stays `pending` until approval, then `active`.  
3. Wallet and COD behave exactly as today.  
4. Webhook + verify are idempotent and amount-safe.  
5. Redirect relays user to frontend with `tran_ref`.  
6. Credentials only via env/config — never hardcoded.  
7. No files changed in `swapo-reference`.

---

## 8. Implementation order (recommended)

1. Phase A (config + `TapService`)  
2. Phase B (pending status + cart_id)  
3. Phase D (routes/controller — can be stubbed against fake HTTP)  
4. Phase C (wire `OnlinePayment` + OrderService + PackageController)  
5. Phase E (brands)  
6. Phase H (tests)  
7. Phase F/G as needed  

---

## 9. Quick reference — Suqna routes to integrate

| Method | Current / new path | Integration |
|--------|--------------------|-------------|
| `POST` | `/api/app/client/orders` | Online → Tap init + pending |
| `POST` | `/api/app/client/package/subscribe` | Online → Tap init + pending |
| `POST` | `/api/app/client/payment/webhook` | **NEW** — Tap callback |
| `GET\|POST` | `/api/app/client/payment/redirect` | **NEW** — frontend relay |
| `POST` | `/api/app/client/payment/verify` | **NEW** — client confirm |
| `GET` | `/api/app/client/wallet` | No change |
| `GET` | `/api/dashboard/admin/payments` | Optional display fields |

---

*Generated from analysis of `swapo-reference` Tap integration and Suqna payment strategy. Update checkboxes as work progresses.*
