# Testing the Kapitano Web ⇄ Kapitano Logistic integration

How to test the integration correctly, in order, from "does the code pass" to "does a real order travel".
Every section says **what you do**, **what you should see**, and **what it means if you don't**.

Three projects are involved:

| Project | Path | Role |
|---|---|---|
| **Shop** (`kapitano_web`) | `C:\xampp\htdocs\kapitano_web` | Laravel store. Hands orders over, follows their progress. Branch `feat/kapitano-logistic-integration-2026-10-07` |
| **Logistic** (`kapitano_logistic`) | `C:\xampp\htdocs\kapitano_logistic` | Laravel API for dispatch, captains, store ledger. Branch `dev` |
| **Dashboard** (`kapitano-admin`) | `C:\Users\DELL\Desktop\Kapitano\kapitano-admin` | Next.js. Back-office screens **and** the store-owner portal (`/portal`). Branch `dev` |

> The full history, decisions and evidence are in the Arabic log: `docs/kapitano-web-integration-implementation-log-ar.md`.

---

## 0. What the integration does (the one-minute model)

```
Shop order confirmed ──► outbox row ──► signed POST /orders ──► Logistic order (pending)
                                                                      │ dispatcher assigns, captain collects/delivers
Shop order status  ◄── signed webhook (order.assigned / picked_up / on_the_way / delivered / cancelled) ◄──┘
```

Rules you will be testing:

1. **Nothing happens unless `LOGISTIC_ENABLED=true`** and the seller is on the allow-list. Safe mode masks customer name/phone.
2. **A Logistic outage never breaks a shop action.** Work is queued in an outbox and retried.
3. **Cancel / edit locks.** Once the captain has collected the goods (`picked_up`, `on_the_way`) the store cannot cancel the order or change its products. The shop screens refuse first; Logistic answers `409` as the backstop.
4. **Who pays the delivery fee.** The customer in the normal case. The **store** when: a free-delivery value band applies, the order is prepaid, the delivery failed, or the order was cancelled **after** pickup. Cancelled **before** pickup costs nothing.
5. **Totals.** Logistic and the portal show *products total*, *delivery fee* (own line), *calculated total* (products + fee) and *what the customer pays at the door*.
6. **One currency.** Syrian pounds (SYP) only. The shop converts from dollars to whole pounds before sending; Logistic refuses any other currency with `422` on create and on edit.

---

## 1. Prerequisites (do this once)

### 1.1 Services that must be running

| Service | Command | Check |
|---|---|---|
| MySQL (XAMPP) | start from XAMPP | `mysql -uroot -e "select 1"` |
| Redis | start Redis | `redis-cli ping` → `PONG` |
| **Logistic API** | in `kapitano_logistic`: `php artisan serve --port=8001` | `http://127.0.0.1:8001/api/integration/v1/health` → **401** (that is correct: it wants a signature) |
| **Shop web** | in `kapitano_web`: `php -S 127.0.0.1:8000 -t public public/index.php` | `http://127.0.0.1:8000/` → 200 |
| **Dashboard** | in `kapitano-admin`: `npm run dev` | `http://localhost:3000/portal/login` → 200 |
| **Demo runner** (only for the manual demo, §4) | in `kapitano_web`: `php tests/Live/support/dev_runner.php` | keeps webhooks and the outbox moving without `queue:work` |

> **Port 8000 must be free for the live scripts (§3).** They start their own shop server. Stop the demo shop server and the demo runner first, and start them again afterwards.

### 1.2 Configuration (shop `.env`)

```env
LOGISTIC_ENABLED=true
LOGISTIC_SAFE_MODE=true            # keep true locally: customer data is masked
LOGISTIC_BASE_URL=http://127.0.0.1:8001/api/integration/v1
LOGISTIC_TOKEN=...                 # the store's machine token (from Logistic: integration:issue-token, or the portal)
LOGISTIC_SIGNING_SECRET=...        # shop → Logistic signature secret
LOGISTIC_WEBHOOK_SECRET=...        # Logistic → shop signature secret
LOGISTIC_ALLOW_SELLERS=<seller id> # start with ONE test seller; LOGISTIC_ALLOW_ALL=false
LOGISTIC_APPLY_STATUS=true         # let Logistic's progress move the shop order (see §5.3)
```

Logistic `.env` for local webhooks to reach the shop: `INTEGRATION_ALLOW_PRIVATE_WEBHOOK_URLS=true` (it refuses local addresses otherwise). The store's callback URL in Logistic must be `http://127.0.0.1:8000/logistic/webhook`.

### 1.3 Database

```
# shop
php artisan migrate
mysql -uroot kapitano_web < sqlupdates/kapitano_logistic_integration_*.sql     # all six files, safe to repeat
php artisan cache:clear
# Logistic
php artisan migrate
```

### 1.4 Quick health check (should all be green before you test anything)

| Command (in the shop) | Expect |
|---|---|
| `php artisan logistic:health` | a signed call to Logistic succeeds (HTTP 200) |
| `php artisan logistic:contract:check` | "matches" — the pinned Swagger equals Logistic's live one. If it reports drift: Logistic's API changed; read the diff, then `logistic:contract:pin` once you accept it |
| `php artisan logistic:status` | counts of outbox rows by state; nothing stuck in `needs_attention` |

---

## 2. Level 1 — Automated tests (no servers needed)

### 2.1 Shop

```bash
cd C:\xampp\htdocs\kapitano_web
# The full suite needs a PHP ini with a bigger memory limit, or it dies silently at 128M while exiting 0
set PHPRC=<folder containing a php.ini with memory_limit=1G>
php artisan test tests/Feature/Logistic tests/Unit/Logistic
```

**Expect:** about **649 tests, all passing** (≈ 3–4 minutes; measured after merging the latest `dev`). Groups worth knowing:

| Test file | Proves |
|---|---|
| `OutboxSenderTest`, `QueueOrderForLogisticTest` | create is queued once, retried on 5xx/network, parked on 422, never duplicated |
| `CancelOrderTest`, `CancelGuardTest` | cancel reaches Logistic; the shop refuses cancel / product edits once the captain has the goods |
| `PaymentSyncTest`, `EditSyncTest` | payment status and basket edits are sent; Logistic's locks are recorded and **not retried** |
| `WebhookReceiverTest`, `StatusConvergerTest`, `StatusFlowTest` | bad signature rejected, duplicates and old events ignored, status applied through the shop's own code |
| `ReconcileCommandTest` | a missed event is recovered by `logistic:reconcile` |
| `Contract/*` | what the shop sends conforms to Logistic's Swagger |

To check the **rest of the shop is untouched**, run the whole suite and compare with a run from before the integration: the existing tests must keep the same result (74 of them already fail on the shop's own, unrelated problems).

### 2.2 Logistic

```bash
cd C:\xampp\htdocs\kapitano_logistic
php -d memory_limit=1G artisan test tests/Feature/Integration tests/Feature/Order tests/Feature/StorePortal tests/Feature/Pricing tests/Feature/Documentation
```

**Expect:** about **467 tests, all passing.** Important ones: `StoreTokenLifetimeTest` (store token does not die at 30 days), `StoreOrderLifecycleTest` (cancel lock), `StoreOrderUpdateTest` (edit locks), `DeliveryFeePricingTest` (fee is the area's full fee; band sets the customer's share), `StoreCancelFeeTest` (cancel after pickup charges the store), `StorePortalOrderTotalsTest`, `OrderTotalTest`, `StorePortalDisclosureTest`.

> The **whole** Logistic suite currently has failures that are **not** from this work (`DispatchSettingKeyTest`, `RoutePlanLifecycleE2ETest`, `PushManagementApiTest`, the `Parallel*` tests). If you see them, they are not regressions of the integration. Also: the `DriverDocumentResubmission` class hangs when run as one block — run it in halves.

### 2.3 Dashboard

```bash
cd C:\Users\DELL\Desktop\Kapitano\kapitano-admin
npx tsc --noEmit
npx eslint src
```

**Expect:** no output (clean).

---

## 3. Level 2 — Live end-to-end scripts (real servers)

These run the **real shop** and the **real Logistic** together and clean up after themselves.

**Before you start:** §1.1 services up, **demo shop server and demo runner stopped** (port 8000 free), `LOGISTIC_SAFE_MODE=true`, database on this machine (the scripts refuse otherwise).

```bash
cd C:\xampp\htdocs\kapitano_web
php tests/Live/logistic_real_flow_e2e.php        # the whole thing: sections 0–18, ≈ 8–10 minutes
```

**Expect the last line:** `198 checks, 0 failed`. Section by section:

| § | What it proves |
|---|---|
| 0–8 | A customer orders through the shop's API, the seller confirms, staff confirm in the admin panel → the order appears in Logistic with correct items/address/amount; confirming twice makes **one** order; other orders are untouched; cancel in the shop cancels in Logistic |
| 9 | Logistic's callbacks move the shop order (assigned → picked up → on the way → delivered) through the shop's own controller |
| 10 | The accountant marking an order paid/unpaid is told to Logistic; the same single row is reused; a missed change is found by `logistic:reconcile` |
| 11 | After pickup: the admin Cancel button and the status dropdown **refuse**; with stale knowledge Logistic answers **409**, parked with a warning; dispatch can still cancel |
| 12 | Quantity edits reach Logistic (basket + amount); after pickup the screen refuses; with stale knowledge Logistic answers **409** `order_already_collected`, recorded, never retried |
| 13–14 | **Real people:** a throw-away dispatcher and captain drive an order via Logistic's real APIs; the shop follows each step; the store ledger gets cash + fee; the portal shows products, the fee line and calculated total |
| 15 | Failed delivery: store charged the fee, shop order unchanged (only warned) |
| 16 | Cancelled after pickup: store charged, captain's cancellation push job queued, captain freed; before pickup: no charge |
| 17 | Free-delivery value band: full fee billed to the store; band removed again |
| 18 | Portal isolation (other store → 404, no login → 401), store-token expiry ≈ 1 year, no contract drift |
| 19 | **Wrapped orders (the customer chooses):** not offered while the store has no wrapping place (and asking anyway is refused, never dropped); offered once the store sets it in the portal; ticked = two linked journeys; not ticked = a normal order with no wrapping field. Also: refused with no wrapping place, accepted once the store sets it in the portal; two linked journeys (collect, then deliver); the delivery cannot be assigned before the goods reach the wrapping place; the shop hears only the delivery; ledger written once; cancelling cancels both legs |

Each older step also has its own script (run one at a time, port 8000 free):

```bash
php tests/Live/logistic_step2_e2e.php   # 84 checks – create, retry, 422/401 handling, Logistic down/up, Swagger enforced
php tests/Live/logistic_step3_e2e.php   # 33 – lost response adopted, reconcile
php tests/Live/logistic_step4_e2e.php   # 40 – cancel paths
php tests/Live/logistic_step5_e2e.php   # 47 – webhook receiver
```

**If a script fails:** read the `FAIL` line — it prints the evidence in brackets. **If you stop a script halfway (Ctrl-C), it leaves test users/orders behind** (names `E2E …`, emails `e2e-…@example.test`): re-running the script's cleanup or `step2` removes them; check `select count(*) from users where email like 'e2e-%'` is 0 afterwards.

---

## 4. Level 3 — Manual walkthrough (you drive it by hand)

Use this to *see* it. It uses fake data only (`demo-*@kapitano.test`, "Kapitano Demo …").

### 4.1 Prepare

```bash
cd C:\xampp\htdocs\kapitano_web
php tests/Live/support/demo.php seed      # creates the demo seller/shop/product/customer/admin and prints the logins
php tests/Live/support/dev_runner.php     # leave running in its own terminal
php tests/Live/support/demo.php status    # what exists
```

Open the shop admin with the demo admin printed by `seed`.

### 4.2 Scenarios — do them in this order

**A. Hand-over.**
1. `php tests/Live/support/demo.php order --confirm` (or place an order, then *Confirm Orders* in the admin).
2. Admin → the order page shows a **Kapitano Logistic** block: state *created*, a Logistic order number, status *pending*.
3. In Logistic (dashboard or DB) the order exists with the right items and **amount to collect**.
   *Expect:* one Logistic order. Pressing confirm again must not create another.

**B. Follow the delivery.** In the Logistic dashboard: assign a captain → (captain app / API) accept → picked up → on the way → delivered.
*Expect:* after each step the shop order page updates (within seconds with the runner). At *delivered* the shop order is delivered, stock and history written as for a manual change.

**C. Cancel before pickup.** New order → cancel it in the shop admin.
*Expect:* cancelled in Logistic; the captain (if assigned) is freed and gets a cancellation notification; **no fee** charged.

**D. Cancel after pickup (the lock).** New order → Logistic: assign, accept, picked up. Then in the shop admin press **Cancel Order**.
*Expect:* refused with *"The captain has already collected this order … Contact Kapitano dispatch"*; order unchanged, no stock moved. Dispatch can still cancel from Logistic → the store is **charged the fee** and sees *"cancelled after pickup"* in the portal.

**E. Edit a quantity.** New order, hand over, then change a line's quantity in the shop admin **before** pickup.
*Expect:* Logistic's order now has the new quantity and amount. After pickup the same action is refused by the shop.

**F. Payment.** Mark an order paid as the accountant.
*Expect:* Logistic shows it paid (the captain must not collect). Mark unpaid again → follows.

**G. Fee and totals in the portal.** Sign in to `http://localhost:3000/portal/login` as `demo-owner@kapitano.test` / `Demo-Passw0rd!` → **Orders** → open an order.
*Expect:* products table, *Products total*, *Delivery fee* (own line; tag **"Paid by you"** with the reason when the store carries it), *Order total* (= products + fee), *The customer pays at the door*.

**H. Back-office total.** In the dashboard order page (admin login): the money card total is **goods + fee**, with *Asked of the customer at the door* beneath and a sentence saying who pays the fee.

**I. Failure isolation.** Stop the Logistic server, confirm an order in the shop.
*Expect:* the **confirmation still succeeds**; the order waits in the outbox (`logistic:status` shows it pending). Start Logistic again, run `php artisan logistic:send` (or wait for the scheduler) → it is created. Nothing was lost or duplicated.

**J. Kill switch.** Set `LOGISTIC_ENABLED=false`, restart the shop, confirm an order.
*Expect:* nothing is queued; the shop behaves exactly as before the integration.

### 4.3 Clean up the demo

```bash
php tests/Live/support/demo.php remove     # cancels the demo orders in Logistic and deletes all demo data
```

---

## 5. Checks you can only do by looking

### 5.1 Fee rules — the five cases

| Case | Make it happen | Portal / dashboard says |
|---|---|---|
| Normal COD order delivered | scenario B | fee on the **customer** (`normal`) |
| Free-delivery value band | in Logistic give the store its own ladder with a band `from 1,000, share 0`, then order ≥ 1,000 | fee on the **store**, "value band"; fee still the area's full fee; customer share 0 |
| Prepaid order | pay online | fee on the **store**, "prepaid" |
| Failed delivery | captain reports failed | fee on the **store**, with the reason on the ledger line |
| Cancelled after pickup / before pickup | scenario D / C | store charged / **nobody charged** |

Also confirm the **store ledger** (portal → Payments → Ledger) has the matching lines: a delivered COD order = a cash line + a negative *delivery fee* line; failed or cancelled-after-pickup = the fee line only.

### 5.2 Security checks

| Check | Expect |
|---|---|
| POST to the shop's `/logistic/webhook` with a wrong or missing signature | rejected (no change) |
| Send the same webhook twice / an older event after a newer one | ignored |
| Portal: open another store's order uuid | **404** (never 403) |
| Portal: no token | **401** |
| A store token against the dispatcher API (`/api/dashboard/…`) | 401/403 |
| Store token expiry (`personal_access_tokens.expires_at` for the store) | about a year ahead |

### 5.3 The "apply status" switch

`LOGISTIC_APPLY_STATUS=false` → the shop only **shows** Logistic's status (display-only). `true` → the shop order really moves (stock, history, notifications). Test **false first** (scenario B shows status on the page but the order's own status does not change), then turn it on for the test seller.

---

## 6. Level 4 — Rolling out for real

1. **Staging first**, with a real Logistic URL, the store's real token and secrets, and a **public** webhook URL set as the store's callback in Logistic.
2. `LOGISTIC_ENABLED=true`, `LOGISTIC_ALLOW_SELLERS=<one seller>`, `LOGISTIC_SAFE_MODE=true`. Place orders; check §4 scenarios A–G.
3. Set pickup coordinates for each real shop (`php artisan logistic:branches` lists them, `logistic:branch:set` sets them). **A shop with none is parked, never guessed.**
4. Production services: `queue:work` running on Logistic (it sends the **Firebase push to the captain** and the webhooks), the Laravel scheduler running on the shop (it sends/retries the outbox every minute), Redis and Reverb on Logistic.
5. Switch `LOGISTIC_SAFE_MODE=false` only when you are ready to contact real customers. Hand-check the **first real orders** (items, address, amount, phone).
6. Widen the allow-list gradually. Instant stop: `LOGISTIC_ENABLED=false` — pending rows stay and resume when turned back on.
7. Run the SQL translation files on the live shop database (§1.3) and clear the cache.

---

## 7. Troubleshooting

| Symptom | Likely cause / what to do |
|---|---|
| Order confirmed but nothing in Logistic | `logistic:status`: outbox row pending → run `logistic:send`; `needs_attention` → read its `last_error` on the order page; check the seller is on the allow-list and `LOGISTIC_ENABLED` |
| `needs_attention` with `cancel_refused:in_transit` / `edit_refused:order_already_collected` | expected lock: the captain has the goods. Contact dispatch; the warning clears itself when Logistic accepts a later change or dispatch cancels |
| Shop order doesn't follow Logistic | `LOGISTIC_APPLY_STATUS` off; or the callback URL isn't reachable; or no `queue:work`/runner on Logistic (webhooks are queued jobs). `logistic:status:sync` polls the quiet ones |
| `401` on every call | token/secret mismatch, or token expired; re-issue the store token |
| Contract drift reported | Logistic's API changed — review, then `logistic:contract:pin` |
| Live script says "port 8000 listens" | stop the demo shop server and runner first |
| Test run exits 0 but shows nothing | PHP memory limit; use a `php.ini` with `memory_limit=1G` |
| Leftover `E2E` data after an interrupted live run | `select count(*) from users where email like 'e2e-%'` (shop) and `admins`/`drivers` named `E2E …` (Logistic); remove them or re-run `logistic_step2_e2e.php` |

---

## 8. What is **not** covered (so you do not assume it was)

- **A real Firebase push to a phone.** Tests only prove the notification job is queued. It needs `queue:work` and a captain device token.
- The **captain mobile app** and **dispatcher dashboard screens**: tested through their APIs, not by clicking.
- **Paymera / real payment gateways** and the real production network, Redis and Reverb.
- A **prepaid order's whole journey live** (the fee going to the store is covered by automated tests only).
- Known limit: a coupon re-split on a *sibling* order after an edit is not noticed.
- Decision still open: the shop does not yet add the delivery fee to the amount it asks the customer to pay at the door; the portal flags the difference as a mismatch.
