# Delivery App Registration + Routes + Full Checklist — Phase 7
### Module registration, sidebar, settings layout, infrastructure

> **Scope:** Wire all new modules into the app — `modules_statuses.json`, `AppRegistry`, sidebar menu,
> settings navigation layout, and full implementation checklist for all phases.
>
> **Prerequisites:** Phases 1–6 complete.
> - `01_courier_driver_plan.md` (Phase 1 + 6)
> - `02_shipping_zone_plan.md` (Phase 2 + 6.3)
> - `03_courier_order_plan.md` (Phase 3 + 4 + 5)

---

## Phase 7 — Module Registration

### Step 7.1 · `modules_statuses.json`

> 🤖 **Agent Prompt:**
> Edit `app/modules_statuses.json`. Add the following entries:
>
> ```json
> "ShippingZone": true,
> "CourierOrder": true,
> "CourierWebhook": true
> ```
>
> The `Delivery` module is already registered. Only add the three new modules.

---

### Step 7.2 · `composer.json` — Autoload

> The root `app/composer.json` already has a merge-plugin glob:
> ```json
> "DeliveryApp/*/composer.json"
> ```
> New modules under `DeliveryApp/` are picked up automatically after:
> ```bash
> composer dump-autoload
> ```
> No manual changes needed.

---

### Step 7.3 · `FeatureManagement/DeliveryApp.php` Seeder

> 🤖 **Agent Prompt:**
> Create `AdminApp/database/seeders/FeatureManagement/DeliveryApp.php`.
> Extend `AbstractAppDefinition`. Use `SalesApp.php` as reference for the pattern.
>
> ```php
> App definition:
>   name    = 'Delivery'
>   slug    = 'deliveries'
>   type    = 'application'
>
> Modules:
>   - name='Courier Providers', slug='delivery_providers'
>   - name='Shipping Zones',    slug='delivery_zones'
>   - name='Courier Orders',    slug='delivery_courier_orders'
>   - name='Delivery Settings', slug='delivery_settings'
> ```
>
> Add `DeliveryApp::definition()` to the `all()` array in:
> `AdminApp/database/seeders/FeatureManagement/Contracts/AppRegistry.php`

---

### Step 7.4 · Sidebar Menu

> 🤖 **Agent Prompt:**
> Edit `resources/js/components/menuItems/menuLists/application-menu-items.tsx`.
>
> Find the existing `Delivery` menu entry. Replace it entirely with:
>
> ```tsx
> {
>     title: 'Delivery',
>     icon: Truck,
>     app: 'deliveries',
>     checkApp: true,
>     items: [
>         {
>             title: 'Courier Providers',
>             icon: Truck,
>             route: 'delivery.index',
>             permission: 'view_deliveries',
>         },
>         {
>             title: 'Shipping Zones',
>             icon: Globe,
>             route: 'shipping-zones.index',
>             permission: 'view_shipping_zones',
>         },
>         {
>             title: 'Courier Orders',
>             icon: Package,
>             route: 'courier-orders.index',
>             permission: 'view_courier_orders',
>         },
>         {
>             title: 'Settings',
>             icon: Settings,
>             route: 'delivery.settings.index',
>             permission: 'platform_delivery_settings',
>         },
>     ],
> }
> ```
>
> Import `Globe` and `Package` from `lucide-react` if not already imported.

---

### Step 7.5 · Delivery Settings Layout (Step 6.3)

> 🤖 **Agent Prompt:**
> Create `resources/js/layouts/delivery-settings-layout.tsx`.
> Mirror the pattern of the existing `inventory-settings-layout.tsx`.
>
> Nav items:
> - **Courier Providers** → `route('delivery.index')`
> - **Shipping Zones**    → `route('shipping-zones.index')`
> - **Courier Orders**    → `route('courier-orders.index')`
> - **Settings**          → `route('delivery.settings.index')`
>
> Use this layout in `DeliverySettings/Index.tsx` and any per-provider credential pages.

---

## Routes Reference

> Quick reference for all delivery-related routes across all modules.

```php
// ============================================================
// PUBLIC — no auth (CourierWebhook/routes/tenant.php)
// ============================================================
Route::post('webhooks/courier/pathao', [CourierWebhookController::class, 'pathao'])
    ->name('webhook.courier.pathao');
Route::post('webhooks/courier/dhl',    [CourierWebhookController::class, 'dhl'])
    ->name('webhook.courier.dhl');
Route::get('track/{trackingCode}',     [TrackingController::class, 'show'])
    ->name('courier.track');

// ============================================================
// PUBLIC API — no auth (ShippingZone/routes/tenant.php)
// ============================================================
Route::get('shipping/districts', fn(Request $request) =>
    response()->json(ShippingDistrict::where('division_id', $request->division_id)
        ->where('is_active', true)->get(['id', 'name']))
)->name('shipping.districts');

// ============================================================
// AUTHENTICATED — (Delivery/routes/tenant.php)
// ============================================================
Route::get('delivery/settings',            [DeliverySettingsController::class, 'index'])
    ->name('delivery.settings.index');
Route::post('delivery/settings',           [DeliverySettingsController::class, 'update'])
    ->name('delivery.settings.update');
Route::get('delivery/{id}/credentials',    [DeliveryController::class, 'showCredentials'])
    ->name('delivery.credentials');
Route::post('delivery/{id}/credentials',   [DeliveryController::class, 'updateCredentials'])
    ->name('delivery.credentials.update');
Route::resource('delivery', DeliveryController::class)
    ->names('delivery');

// ============================================================
// AUTHENTICATED — (ShippingZone/routes/tenant.php)
// ============================================================
Route::post('shipping-zones/bulk-action',  [ShippingZoneController::class, 'bulkAction'])
    ->name('shipping-zones.bulk-action');
Route::resource('shipping-zones', ShippingZoneController::class)
    ->names('shipping-zones');
Route::post('shipping-zones/{zoneId}/rates',        [ShippingZoneRateController::class, 'store'])
    ->name('shipping-zone-rates.store');
Route::put('shipping-zones/{zoneId}/rates/{id}',    [ShippingZoneRateController::class, 'update'])
    ->name('shipping-zone-rates.update');
Route::delete('shipping-zones/{zoneId}/rates/{id}', [ShippingZoneRateController::class, 'destroy'])
    ->name('shipping-zone-rates.destroy');

// ============================================================
// AUTHENTICATED — (CourierOrder/routes/tenant.php)
// ============================================================
Route::post('courier-orders/bulk-action',      [CourierOrderController::class, 'bulkAction'])
    ->name('courier-orders.bulk-action');
Route::post('courier-orders/export',           [CourierOrderController::class, 'export'])
    ->name('courier-orders.export');
Route::post('courier-orders/{id}/dispatch',    [CourierOrderController::class, 'dispatch'])
    ->name('courier-orders.dispatch');
Route::put('courier-orders/{id}/status',       [CourierOrderController::class, 'updateStatus'])
    ->name('courier-orders.status');
Route::post('courier-orders/{id}/sync',        [CourierOrderController::class, 'sync'])
    ->name('courier-orders.sync');
Route::resource('courier-orders', CourierOrderController::class)
    ->names('courier-orders');
```

---

## Complete Implementation Checklist

### Phase 1 — Extend Delivery Module (`01_courier_driver_plan.md`)
- [ ] Migration: alter `deliveries` (provider_key, credentials, supports_cod, supports_webhook, is_test_mode)
- [ ] `Delivery` model: new fillable + `encrypted:array` cast + `scopeActive()`
- [ ] `SettingKeys` constants class at `app/Support/SettingKeys.php`
- [ ] `CourierApiException` at `app/Exceptions/CourierApiException.php`
- [ ] `CourierSettingService` (wraps existing Setting model)
- [ ] `CourierDriverInterface` contract
- [ ] `SteadfastDriver` (API key auth, poll-only)
- [ ] `PathaoDriver` (OAuth2, token stored in credentials column, `ensureToken()`)
- [ ] `DhlDriver` (Basic auth, PDF label stored in `storage/app/public/courier-labels/`)
- [ ] `StatusNormalizer` (all 3 providers → DeliveryOrderStatusEnum)
- [ ] `CourierManager` singleton registered in `DeliveryServiceProvider`
- [ ] `CourierProviderSeeder` (3 rows: Steadfast, Pathao, DHL)

### Phase 2 — ShippingZone Module (`02_shipping_zone_plan.md`)
- [ ] Module `DeliveryApp/ShippingZone` scaffolded + registered
- [ ] 5 migrations (countries, divisions, districts, zones, zone_rates)
- [ ] 5 models with correct relationships, casts, Filterable
- [ ] `ShippingZoneFilter` (search, countryId, divisionId + CommonFilter)
- [ ] `ShippingZoneService` (resolveForAddress, calculateShippingCost, baseQuery, paginate)
- [ ] `ShippingZoneResource` + `ShippingZoneRateResource`
- [ ] `ShippingZoneController` (wantsJson + Inertia + bulkAction)
- [ ] `ShippingZoneRateController` (store/update/destroy)
- [ ] Routes (bulk-action before resource, rate sub-routes, districts JSON endpoint)
- [ ] `ShippingZone/Index.tsx` (CLAUDE.md layout, DataTable, stats, inline rates panel)
- [ ] `ShippingCountrySeeder` (seed ISO countries, BD divisions, BD districts)

### Phase 3 — CourierOrder Module (`03_courier_order_plan.md`)
- [ ] Module `DeliveryApp/CourierOrder` scaffolded + registered
- [ ] 2 migrations (`courier_orders`, `courier_order_tracking_events`)
- [ ] `CourierOrder` model: Filterable, SoftDeletes, nonTerminal scope, cross-module relations
- [ ] `CourierOrderTrackingEvent` model: append-only, no SoftDeletes
- [ ] `CourierOrderFilter` (search, providerKey, deliveryId, orderId, date ranges + CommonFilter)
- [ ] `CourierOrderService` (createFromOrder, dispatch, updateStatus, syncTracking, buildPayload)
- [ ] `CourierOrderController` (all standard + dispatch/updateStatus/sync — all redirect)
- [ ] Routes: action routes declared BEFORE `Route::resource`
- [ ] `CourierOrder/Index.tsx` (5 stat cards, DataTable, filters, bulk actions)
- [ ] `CourierOrder/Show.tsx` (gradient header, conditional buttons, tracking timeline)
- [ ] `CourierOrder/Create.tsx` (order AJAX select, courier select, form fields)
- [ ] 3 Events declared
- [ ] 3 Listeners registered in EventServiceProvider
- [ ] `CourierDispatchedMail`, `CourierDeliveredMail` mail classes

### Phase 4 — CourierWebhook Module (`03_courier_order_plan.md`)
- [ ] Module `DeliveryApp/CourierWebhook` scaffolded + registered
- [ ] `VerifyCsrfToken`: webhook paths added to `$except`
- [ ] `CourierWebhookController` (HMAC verify sync → 401 on bad sig → queue → 200)
- [ ] `ProcessCourierWebhookJob` (queue: courier-webhooks, tenant init, dedup events)
- [ ] `PollCourierStatusJob` (non-terminal, last_polled >28 min, limit 50)
- [ ] `SyncSingleCourierTrackingJob` (silent on CourierApiException — no failed_jobs fill)
- [ ] `SyncCourierStatuses` Artisan command (`courier:sync`)
- [ ] Schedule: `everyThirtyMinutes()->onOneServer()` in `CourierWebhookServiceProvider`
- [ ] `TrackingController` (public, no auth, by tracking_code or consignment_id)
- [ ] `Tracking/Show.tsx` (no AppLayout, public page, not-found state)
- [ ] Public routes in tenant.php (no auth group)

### Phase 5 — Notifications (`03_courier_order_plan.md`)
- [ ] `CourierOrderDispatchedEvent`
- [ ] `CourierOrderStatusChangedEvent`
- [ ] `CourierOrderDeliveredEvent`
- [ ] `SendDispatchNotificationListener` (queued)
- [ ] `SendStatusChangeNotificationListener` (queued — only for on_the_way/delivered/failed)
- [ ] `UpdateOrderShippingTimestampsListener` (synchronous — writes `orders.delivered_at`)
- [ ] All registered in `CourierOrder/Providers/EventServiceProvider.php`

### Phase 6 — Settings UI (`01_courier_driver_plan.md` + `02_shipping_zone_plan.md`)
- [ ] `DeliverySettingsController` (global settings — whitelisted keys only)
- [ ] `DeliverySettings/Index.tsx` (settings form + provider list with credential links)
- [ ] Credential endpoints on `DeliveryController` (per-provider key whitelist)
- [ ] `delivery-settings-layout.tsx` (mirrors inventory-settings-layout pattern)

### Phase 7 — Registration (this file)
- [ ] `modules_statuses.json` updated (ShippingZone, CourierOrder, CourierWebhook = true)
- [ ] `composer dump-autoload` run after new modules added
- [ ] `FeatureManagement/DeliveryApp.php` seeder created
- [ ] `AppRegistry.php` — DeliveryApp entry added
- [ ] Sidebar `application-menu-items.tsx` updated (Courier Providers, Shipping Zones, Courier Orders, Settings)

### Phase 8 — Tests (summary — details in each phase file)
- [ ] `ShippingZoneTest` — zone resolution priority, rate calculation, free shipping threshold
- [ ] `SteadfastDriverTest` — Http::fake, 422 → CourierApiException, 503 retry
- [ ] `StatusNormalizerTest` — all 3 providers full mapping + unknown → pending
- [ ] `WebhookTest` — HMAC valid/invalid, dedup, unknown consignment silent
- [ ] `CourierOrderTest` — dispatch, status sync, terminal skip, UpdateOrderTimestamps sync
- [ ] `CourierSettingsTest` — encrypt/decrypt, key whitelist enforcement

---

## Infrastructure Checklist

These must be configured in deployment/docker setup before running the shipping system in production.

- [ ] **Queue workers running for both queues:**
  ```bash
  php artisan queue:work --queue=courier-webhooks
  php artisan queue:work --queue=courier-tracking
  ```
  Or combined: `php artisan queue:work --queue=courier-webhooks,courier-tracking,default`

- [ ] **Scheduler configured:**
  Cron entry: `* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1`
  Runs `PollCourierStatusJob` every 30 minutes for Steadfast polling.

- [ ] **Storage link created** (for DHL PDF labels):
  ```bash
  php artisan storage:link
  ```

- [ ] **Redis configured** for queue driver (`QUEUE_CONNECTION=redis` in `.env`).

- [ ] **APP_KEY set per tenant** — the `encrypted:array` cast on `credentials` uses this key.
  All tenants must have a stable `APP_KEY` for credential encryption to survive restarts.

- [ ] **Webhook URLs registered with couriers:**
  - Pathao: `https://{tenant-domain}/webhooks/courier/pathao`
  - DHL: `https://{tenant-domain}/webhooks/courier/dhl`
  - Steadfast: no webhook (polling only via scheduler)

- [ ] **Webhook secrets stored in settings** (via Delivery Settings UI):
  - `delivery.webhook_secret_pathao` → Pathao merchant webhook secret
  - `delivery.webhook_secret_dhl` → DHL API secret

---

## Module Dependency Map

```
Phase 1 (Delivery extension)
    ↓ CourierManager, StatusNormalizer, CourierDriverInterface
Phase 2 (ShippingZone) — parallel to Phase 1 (no dependency)
    ↓ ShippingZoneService::resolveForAddress()
Phase 3 (CourierOrder) — depends on Phase 1
    CourierOrderService::dispatch() → CourierManager (Phase 1)
    CourierOrderService::createFromOrder() → ShippingZoneService (Phase 2, optional)
    ↓ Events, Listeners
Phase 4 (CourierWebhook) — depends on Phase 3
    ProcessCourierWebhookJob → CourierOrderService (Phase 3)
    PollCourierStatusJob → CourierOrderService (Phase 3)
Phase 5 (Notifications) — depends on Phase 3
    Listeners → Events from Phase 3
Phase 6 (Settings UI) — depends on Phase 1 (CourierSettingService)
Phase 7 (Registration) — depends on all phases complete
```

---

## Key Design Decisions (summary)

**Why extend `deliveries` not create `courier_providers`?**
The `deliveries` table already provides CRUD, activity logging, soft deletes, status management, and export. Adding columns reuses all of this. `encrypted:array` keeps API keys per-provider, per-tenant.

**Why not `.env` for credentials?**
Multi-tenant: every tenant has their own courier accounts. `.env` is global. All credentials live in the `credentials` column (AES-256-CBC via APP_KEY), naturally scoped per tenant database.

**Why 401 on invalid webhook signature?**
The "always 200" rule exists so legitimate couriers don't stop retrying on transient failures. A bad HMAC is a security event, not a transient error. 401 stops retry storms and alerts developers. Real couriers will not retry on 401.

**Why HMAC verified synchronously before queuing?**
Verification is a security gate. Queuing unverified payloads would let malicious actors fill the queue cheaply. Verify → reject fast → queue only on success.

**Why `SyncSingleCourierTrackingJob` swallows `CourierApiException`?**
Polling failures (API timeouts, rate limits) are routine. Letting them fill `failed_jobs` creates noise and wastes retry cycles. Log + move on — the next 30-minute cycle retries naturally.

**Why `UpdateOrderShippingTimestampsListener` is synchronous?**
Race condition: if queued, the queue worker might write `delivered_at` after the user's next page load, showing stale data. Synchronous write ensures the timestamp exists before the HTTP response.

**Why zone priority cascade: district → division → country?**
Lets admins set district-level rates for high-volume cities (Dhaka, Chittagong) with division fallback for the rest. Country is the catch-all. Priority column overrides the hierarchy for special cases.
