{
    "info": {
        "_postman_id": "9f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
        "name": "Kapitano — Store Portal (Feature 09)",
        "description": "The store portal end to end: a shop applies, is approved, collects its own credentials, pushes a real signed order, watches it delivered, reads the money and gets paid.\n\n---\n\n## Run it in order\n\nThe folders are numbered because they hand things to each other. **Collection Runner, top to bottom, works as a full end-to-end test** — every request saves what the next one needs (tokens, uuids, the signing secret) into collection variables, so you should not have to copy a single value by hand.\n\nIf you only want one request, note what it depends on:\n\n| Folder | Needs |\n|---|---|\n| 01 settings | a portal token from `00 → Sign in` |\n| 03 back office | an admin token from `03 → Sign in (admin)` |\n| 04 machine API | the store approved (03) **and** `signing_secret` revealed (01) |\n| 05–06 portal reads | the store approved, and an order from 04 |\n| 07 settlements | an approved store |\n\n---\n\n## Two things that are done for you\n\n**Both required headers.** `Accept: application/json` and `Accept-Language` go on every request from the collection pre-request script. Without both, *every* endpoint in this application answers `401` — before authentication is even attempted — which looks like a credentials problem and is not.\n\n**The HMAC signature.** Requests to `/api/integration/v1/*` are signed automatically from `signing_secret`. The script signs the body **after** resolving `{{variables}}` and pins the resolved body onto the request, because the signature covers the exact bytes that go on the wire — re-encoding JSON after signing is the single most common integration fault.\n\n---\n\n## Which credential is which\n\nThree different things authenticate here, and mixing them up is the usual first hour:\n\n| Token | Who | Where it comes from |\n|---|---|---|\n| `portal_token` | a **person** at the shop | `POST /store-portal/auth/login` |\n| `admin_token` | our **back office** | `POST /dashboard/auth/login` |\n| `store_api_token` | the shop's **servers** | `php artisan integration:issue-token <slug>` — never over HTTP |\n\nThe last one is deliberate: approving a store does not mint its API credential, because a plaintext token can only be read once and should not appear in a response nobody asked for.",
        "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
    },
    "event": [
        {
            "listen": "prerequest",
            "script": {
                "type": "text/javascript",
                "exec": [
                    "// ── Both mandatory headers, on everything ────────────────────────────────────────────",
                    "// CheckApiHeaderMiddleware answers 401 without BOTH of these, before auth runs. A missing",
                    "// header and a bad token therefore look identical until you read MessageDebug.",
                    "pm.request.headers.upsert({ key: 'Accept', value: 'application/json' });",
                    "pm.request.headers.upsert({",
                    "    key: 'Accept-Language',",
                    "    value: pm.collectionVariables.get('locale') || 'en'",
                    "});",
                    "",
                    "// ── Sign requests to the machine API ─────────────────────────────────────────────────",
                    "const url = pm.request.url.toString();",
                    "",
                    "if (!url.includes('/api/integration/v1')) {",
                    "    return;",
                    "}",
                    "",
                    "const secret = pm.collectionVariables.get('signing_secret');",
                    "",
                    "if (!secret) {",
                    "    console.warn('No signing_secret yet. Run \"01 → Reveal secrets\" first — it saves it for you.');",
                    "    return;",
                    "}",
                    "",
                    "// The signature covers the EXACT bytes that go on the wire, so resolve {{variables}}",
                    "// first and pin the result back onto the request. Signing the unresolved template and",
                    "// letting Postman substitute afterwards is the classic way to get signature_mismatch.",
                    "let raw = '';",
                    "",
                    "if (pm.request.body && pm.request.body.mode === 'raw' && pm.request.body.raw) {",
                    "    raw = pm.variables.replaceIn(pm.request.body.raw);",
                    "    pm.request.body.raw = raw;",
                    "}",
                    "",
                    "const t = Math.floor(Date.now() / 1000);",
                    "const v1 = CryptoJS.HmacSHA256(t + '.' + raw, secret).toString(CryptoJS.enc.Hex);",
                    "",
                    "pm.request.headers.upsert({ key: 'X-Kapitano-Signature', value: 't=' + t + ',v1=' + v1 });",
                    "",
                    "// Every POST to the store API needs one. A repeat of the same key replays the stored",
                    "// answer instead of creating a second order, which is what makes a retry safe.",
                    "if (pm.request.method === 'POST') {",
                    "    pm.request.headers.upsert({ key: 'Idempotency-Key', value: require('uuid').v4() });",
                    "}"
                ]
            }
        },
        {
            "listen": "test",
            "script": {
                "type": "text/javascript",
                "exec": [
                    "// Every response in this application uses the same envelope, so one assertion covers",
                    "// the lot and a failure points at the response rather than at a parse error.",
                    "pm.test('answers the standard envelope', function () {",
                    "    pm.response.to.be.json;",
                    "    pm.expect(pm.response.json()).to.have.property('Status');",
                    "});",
                    "",
                    "// Refusals carry their reason as a KEY in MessageDebug. Surface it — the message text is",
                    "// translated and changes with Accept-Language; the key is the contract.",
                    "if (pm.response.code >= 400) {",
                    "    const debug = pm.response.json().MessageDebug;",
                    "",
                    "    if (debug && typeof debug === 'object') {",
                    "        console.log('refused: ' + Object.keys(debug).join(', '));",
                    "    }",
                    "}"
                ]
            }
        }
    ],
    "variable": [
        {
            "key": "base_url",
            "value": "http://127.0.0.1:8000",
            "type": "string"
        },
        {
            "key": "locale",
            "value": "en",
            "type": "string"
        },
        {
            "key": "store_email",
            "value": "owner@freshmarket.test",
            "type": "string"
        },
        {
            "key": "store_password",
            "value": "correct-horse-9",
            "type": "string"
        },
        {
            "key": "admin_email",
            "value": "admin@kapitano.test",
            "type": "string"
        },
        {
            "key": "admin_password",
            "value": "password",
            "type": "string"
        },
        {
            "key": "webhook_url",
            "value": "https://api.freshmarket.test/kapitano/webhooks",
            "type": "string"
        },
        {
            "key": "portal_token",
            "value": "",
            "type": "string"
        },
        {
            "key": "admin_token",
            "value": "",
            "type": "string"
        },
        {
            "key": "store_api_token",
            "value": "",
            "type": "string"
        },
        {
            "key": "signing_secret",
            "value": "",
            "type": "string"
        },
        {
            "key": "webhook_secret",
            "value": "",
            "type": "string"
        },
        {
            "key": "store_uuid",
            "value": "",
            "type": "string"
        },
        {
            "key": "store_user_uuid",
            "value": "",
            "type": "string"
        },
        {
            "key": "staff_uuid",
            "value": "",
            "type": "string"
        },
        {
            "key": "order_uuid",
            "value": "",
            "type": "string"
        },
        {
            "key": "external_order_id",
            "value": "",
            "type": "string"
        },
        {
            "key": "delivery_uuid",
            "value": "",
            "type": "string"
        },
        {
            "key": "settlement_uuid",
            "value": "",
            "type": "string"
        },
        {
            "key": "captain_uuid",
            "value": "",
            "type": "string"
        },
        {
            "key": "otp_code",
            "value": "",
            "type": "string"
        }
    ],
    "item": [
        {
            "name": "00 — Onboarding",
            "description": "A shop that has never heard of us, applying.\n\nWhat registration creates is an **application**, never an integration: the store is `pending` and inactive and no API token exists. That is the whole reason a public registration endpoint is safe to have.",
            "item": [
                {
                    "name": "Register a store",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "pm.test('created', () => pm.response.to.have.status(201));",
                                    "",
                                    "const model = pm.response.json().Model;",
                                    "pm.collectionVariables.set('store_user_uuid', model.uuid);",
                                    "",
                                    "if (model.store) {",
                                    "    pm.collectionVariables.set('store_uuid', model.store.uuid);",
                                    "",
                                    "    pm.test('is an application, not a live integration', function () {",
                                    "        pm.expect(model.store.status).to.eql('pending');",
                                    "        pm.expect(model.store.is_live).to.eql(false);",
                                    "    });",
                                    "}",
                                    "",
                                    "pm.test('the applicant became the owner', () => pm.expect(model.role).to.eql('owner'));"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"store_name\": \"Fresh Market\",\n  \"name\": \"Layla Q\",\n  \"email\": \"{{store_email}}\",\n  \"phone\": \"966500000001\",\n  \"password\": \"{{store_password}}\",\n  \"password_confirmation\": \"{{store_password}}\",\n  \"default_currency\": \"SAR\",\n  \"timezone\": \"Asia/Riyadh\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/auth/register",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "auth",
                                "register"
                            ]
                        },
                        "description": "No token comes back. Handing one over here would imply the shop is live when it is not — sign in afterwards like anybody else.\n\n**The handle is derived, not chosen.** Sending a `slug` does nothing. Letting an applicant pick one and telling them it is taken turns this endpoint into a directory of which shops use us.\n\n`webhook_url` is optional here so a shop with no receiver yet can still apply. Send one and it goes through the same SSRF check as the settings screen."
                    }
                },
                {
                    "name": "Sign in",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "pm.test('signed in', () => pm.response.to.have.status(200));",
                                    "",
                                    "const model = pm.response.json().Model;",
                                    "pm.collectionVariables.set('portal_token', model.token);",
                                    "pm.collectionVariables.set('store_user_uuid', model.user.uuid);",
                                    "",
                                    "if (model.user.store) {",
                                    "    pm.collectionVariables.set('store_uuid', model.user.store.uuid);",
                                    "}",
                                    "",
                                    "// A screen should read this rather than re-deriving the role rules. If it is false,",
                                    "// every owner-only route will answer 403.",
                                    "console.log('can manage integration: ' + model.user.can_manage_integration);"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"email\": \"{{store_email}}\",\n  \"password\": \"{{store_password}}\",\n  \"device_name\": \"postman\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/auth/login",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "auth",
                                "login"
                            ]
                        },
                        "description": "Works **while the store is still under review** — deliberately. A pending owner reaches `settings/*` and nothing else, so they can stand their receiver up while the review runs instead of after it.\n\nAn unknown address and a wrong password answer identically. Anything else would make this a directory of which shops deliver with us.\n\nSaves `portal_token` for you."
                    }
                },
                {
                    "name": "My profile",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/profile",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "profile"
                            ]
                        },
                        "description": "The signed-in person with their store attached. Both roles reach this — what is behind it is the caller's own record, so holding a token for it is the whole of the authorisation."
                    }
                },
                {
                    "name": "Orders — refused while pending",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "pm.test('403 until the store is approved', () => pm.response.to.have.status(403));",
                                    "pm.test('and says why', function () {",
                                    "    pm.expect(pm.response.json().MessageDebug).to.have.property('store_not_approved');",
                                    "});"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/orders?rows=25&page=1",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "orders"
                            ],
                            "query": [
                                {
                                    "key": "rows",
                                    "value": "25"
                                },
                                {
                                    "key": "page",
                                    "value": "1"
                                }
                            ]
                        },
                        "description": "**Expected to fail, with 403 `store_not_approved`.** Run it before the approval in folder 03 to see the gate working.\n\nA 403 rather than an empty list on purpose: `[]` reads as \"we lost your orders\", which sends the shop to support for the wrong reason."
                    }
                }
            ]
        },
        {
            "name": "01 — Settings (owner only)",
            "description": "The integration a shop configures for itself. Reachable while still under review — these are the screens a pending store is let in for.\n\nNo request here takes a store identifier: the store is resolved from the caller's own token, so there is nothing to point at somebody else's.",
            "item": [
                {
                    "name": "Show settings",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/settings",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "settings"
                            ]
                        },
                        "description": "Returns **secret metadata only** — whether they exist, when they were last rotated, and when the previous pair stops being accepted. Never the values; those need the password."
                    }
                },
                {
                    "name": "Set the callback URL",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"webhook_url\": \"{{webhook_url}}\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/settings/webhook",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "settings",
                                "webhook"
                            ]
                        },
                        "description": "Must be **https**, on a **public** host, standard port, no credentials in the URL.\n\nThat is not fussiness. *We* make the request, from inside our own network, to whatever is typed — so `https://169.254.169.254/...` would turn this field into a reader for our server's cloud credentials. Try it and watch the 422.\n\nThe host is re-checked again at the moment each callback is sent, because a name that resolves publicly today can resolve privately tomorrow."
                    }
                },
                {
                    "name": "Set the callback URL — private address (refused)",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "pm.test('refused', () => pm.response.to.have.status(422));"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"webhook_url\": \"https://169.254.169.254/latest/meta-data/\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/settings/webhook",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "settings",
                                "webhook"
                            ]
                        },
                        "description": "**Expected to fail.** The cloud instance-metadata address — the canonical SSRF target. Also try `https://127.0.0.1/hook`, `http://store.test/hook` (plaintext), `https://user:pass@store.test/hook` (credentials) and `https://store.test:8443/hook` (non-standard port); each is refused for its own reason."
                    }
                },
                {
                    "name": "Set the allowed addresses",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"allowed_ips\": []\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/settings/ips",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "settings",
                                "ips"
                            ]
                        },
                        "description": "An **empty array switches the check off**, which is how onboarding runs and what this request sends.\n\nThis gates the machine API, not the portal — so getting it wrong locks your servers out while leaving you able to sign in here and fix it. Put your real source addresses in once the integration is live."
                    }
                },
                {
                    "name": "Reveal the secrets",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "pm.test('revealed', () => pm.response.to.have.status(200));",
                                    "",
                                    "const model = pm.response.json().Model;",
                                    "pm.collectionVariables.set('signing_secret', model.signing_secret);",
                                    "pm.collectionVariables.set('webhook_secret', model.webhook_secret);",
                                    "",
                                    "pm.test('the answer is not cacheable', function () {",
                                    "    pm.expect(pm.response.headers.get('Cache-Control') || '').to.include('no-store');",
                                    "});"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"password\": \"{{store_password}}\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/settings/secrets/reveal",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "settings",
                                "secrets",
                                "reveal"
                            ]
                        },
                        "description": "**Asks for the password again.** A stolen session must not be worth as much as a stolen signing key, and re-authenticating is the cheapest thing that keeps them apart.\n\n- `signing_secret` — what **your servers** sign requests to us with. Saved into the collection, and folder 04 signs with it automatically.\n- `webhook_secret` — what **we** sign callbacks to you with.\n\nThe response is `no-store`. Do not log it."
                    }
                },
                {
                    "name": "Send a test callback",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "if (pm.response.code === 200) {",
                                    "    pm.collectionVariables.set('delivery_uuid', pm.response.json().Model.uuid);",
                                    "}"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/settings/webhook/test",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "settings",
                                "webhook",
                                "test"
                            ]
                        },
                        "description": "Queues a real `integration.ping` — the same queue, signature and delivery ledger a real event uses. A test that took a shortcut would pass while the real path was broken.\n\nIt carries **no `order` and no `sequence`**, so make sure your handler tolerates both being absent.\n\n**A queue worker must be running** or it will sit at `pending` forever: `php artisan queue:work`. Watch the result in folder 05.\n\n`409 webhook_url_missing` if you have not set a URL yet."
                    }
                },
                {
                    "name": "Rotate the secrets",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "if (pm.response.code !== 200) { return; }",
                                    "",
                                    "const model = pm.response.json().Model;",
                                    "",
                                    "// Keep the OLD secret in a variable so you can prove the overlap window works:",
                                    "// push an order in folder 04 signed with it and watch it still be accepted.",
                                    "pm.collectionVariables.set('previous_signing_secret', pm.collectionVariables.get('signing_secret'));",
                                    "",
                                    "pm.collectionVariables.set('signing_secret', model.signing_secret);",
                                    "pm.collectionVariables.set('webhook_secret', model.webhook_secret);"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"password\": \"{{store_password}}\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/settings/secrets/rotate",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "settings",
                                "secrets",
                                "rotate"
                            ]
                        },
                        "description": "**This does not cause an outage**, which is the whole point of having the button.\n\nFor 24 hours afterwards:\n\n- inbound, we accept a request signed with **either** secret;\n- outbound, every callback is signed with **both**, and the header carries two `v1` values.\n\nSo deploy the new key on your own schedule. Your verifier must **iterate the `v1` entries** rather than read the first — if it reads only the first, your callbacks start failing the moment anybody presses this.\n\nThe old secret is saved as `previous_signing_secret`; point folder 04 at it to watch the overlap window work."
                    }
                }
            ]
        },
        {
            "name": "02 — Staff",
            "description": "The colleagues an owner adds. Owner only.\n\n**No password is set by anybody.** An invited person gets an account with one nobody knows, and signs in the first time by asking for a recovery code — which goes to the phone number the owner typed. Get that number right.",
            "item": [
                {
                    "name": "List the people here",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/staff?rows=25&page=1",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "staff"
                            ],
                            "query": [
                                {
                                    "key": "rows",
                                    "value": "25"
                                },
                                {
                                    "key": "page",
                                    "value": "1"
                                }
                            ]
                        }
                    }
                },
                {
                    "name": "Invite a colleague",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "if (pm.response.code !== 201) { return; }",
                                    "",
                                    "const model = pm.response.json().Model;",
                                    "pm.collectionVariables.set('staff_uuid', model.uuid);",
                                    "",
                                    "pm.test('always staff, never a second owner', () => pm.expect(model.role).to.eql('staff'));"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"name\": \"Nadia\",\n  \"email\": \"nadia@freshmarket.test\",\n  \"phone\": \"966500000099\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/staff",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "staff"
                            ]
                        },
                        "description": "Sending `role` or `password` does nothing.\n\n`role` is ignored because an owner minting another owner is how a removed employee keeps their access. `password` is ignored because an owner who knows a colleague's password can act as them.\n\nThe invitee signs in by running `08 → Forgot password` with their own address."
                    }
                },
                {
                    "name": "Turn a colleague off",
                    "request": {
                        "method": "PATCH",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"is_active\": false\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/staff/{{staff_uuid}}/activation",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "staff",
                                "{{staff_uuid}}",
                                "activation"
                            ]
                        },
                        "description": "Revokes their sessions immediately rather than waiting for a token to expire — Sanctum tokens do not expire on their own.\n\nTry it on **your own** uuid: `409 cannot_disable_self`. The owner is the only role that can change the integration, so locking yourself out would need somebody else to undo.\n\nSomebody at another store answers **404**, not 403."
                    }
                }
            ]
        },
        {
            "name": "03 — Back office: review",
            "description": "Our side. This is the gate the public registration endpoint rests on: nothing turns an application into a live integration except an admin here.\n\nThree permissions, because three different people do the jobs: `store_clients.view` to read, `.review` to decide, `.manage` to pause a store that is already trading.",
            "item": [
                {
                    "name": "Sign in (admin)",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "pm.test('signed in', () => pm.response.to.have.status(200));",
                                    "pm.collectionVariables.set('admin_token', pm.response.json().Model.token);"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"email\": \"{{admin_email}}\",\n  \"password\": \"{{admin_password}}\",\n  \"device_name\": \"postman\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/auth/login",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "auth",
                                "login"
                            ]
                        },
                        "description": "Set `admin_email` / `admin_password` to a real back-office account with the `store_clients.*` and `settlements.*` permissions. Seed them with `php artisan db:seed --class=PermissionSeeder` if the permissions are missing."
                    }
                },
                {
                    "name": "The review queue",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "const rows = pm.response.json().Model;",
                                    "",
                                    "if (Array.isArray(rows) && rows.length && !pm.collectionVariables.get('store_uuid')) {",
                                    "    pm.collectionVariables.set('store_uuid', rows[0].uuid);",
                                    "}"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/store-clients?rows=25&page=1&status=pending",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "store-clients"
                            ],
                            "query": [
                                {
                                    "key": "rows",
                                    "value": "25"
                                },
                                {
                                    "key": "page",
                                    "value": "1"
                                },
                                {
                                    "key": "status",
                                    "value": "pending",
                                    "description": "pending | approved | rejected | suspended. Omit for all."
                                },
                                {
                                    "key": "search",
                                    "value": "",
                                    "description": "Name or handle.",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "Deliberately not the inherited paginate, which hides inactive rows — an application awaiting review is inactive by definition, so that queue would always look empty."
                    }
                },
                {
                    "name": "One store",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/store-clients/{{store_uuid}}",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "store-clients",
                                "{{store_uuid}}"
                            ]
                        },
                        "description": "No secret appears here either. Support can read a store's configuration without being able to read its signing key."
                    }
                },
                {
                    "name": "Its people",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/store-clients/{{store_uuid}}/users?rows=25&page=1",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "store-clients",
                                "{{store_uuid}}",
                                "users"
                            ],
                            "query": [
                                {
                                    "key": "rows",
                                    "value": "25"
                                },
                                {
                                    "key": "page",
                                    "value": "1"
                                }
                            ]
                        },
                        "description": "\"Approved store, nobody can sign in\" is the onboarding failure that looks like nothing at all from our side. This is where you check."
                    }
                },
                {
                    "name": "Approve",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "pm.test('approved', () => pm.response.to.have.status(200));",
                                    "",
                                    "const model = pm.response.json().Model;",
                                    "pm.test('and live', () => pm.expect(model.is_live).to.eql(true));",
                                    "",
                                    "console.log('Now issue the machine credential on the server:');",
                                    "console.log('  php artisan integration:issue-token ' + model.slug);"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"note\": \"Contract signed\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/store-clients/{{store_uuid}}/approve",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "store-clients",
                                "{{store_uuid}}",
                                "approve"
                            ]
                        },
                        "description": "Switches the store on and moves every `pending` person at it to `active`.\n\n**It does not mint the API token.** A plaintext token can only be read once, so it is issued deliberately rather than printed into a response nobody asked for:\n\n```\nphp artisan integration:issue-token <slug>\n```\n\nPut that token into the `store_api_token` variable before running folder 04. The console log above prints the exact command with the right handle."
                    }
                },
                {
                    "name": "Reject",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"note\": \"Could not verify the business\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/store-clients/{{store_uuid}}/reject",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "store-clients",
                                "{{store_uuid}}",
                                "reject"
                            ]
                        },
                        "description": "**The note is required here**, unlike on an approval. This is one of the two decisions a shop rings up about, and \"no reason recorded\" makes that call unanswerable by whoever picks it up.\n\nRevokes every portal session at the store immediately.\n\nRejecting an already-approved store answers `409` — approved leads only to suspended. Re-deciding something somebody already decided is a mistake, not a workflow."
                    }
                },
                {
                    "name": "Suspend",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"note\": \"Unpaid invoices\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/store-clients/{{store_uuid}}/suspend",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "store-clients",
                                "{{store_uuid}}",
                                "suspend"
                            ]
                        },
                        "description": "Stops a store trading **without unpicking its approval** — a pause is meant to end, and a suspended store is not a rejected one.\n\nNeeds `store_clients.manage`, which reviewing does not grant: pausing a shop that is already trading stops another company's orders and belongs to operations.\n\nTakes effect on the very next request. Run it, then retry anything in folder 05."
                    }
                }
            ]
        },
        {
            "name": "04 — Machine API: push an order",
            "description": "The shop's **servers**, not its staff. Different credential, and every request is HMAC-signed — the collection does that for you from `signing_secret`.\n\nSet `store_api_token` first: `php artisan integration:issue-token <slug>` on the server. Approving a store does not mint it.",
            "item": [
                {
                    "name": "Health",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{store_api_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/integration/v1/health",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "integration",
                                "v1",
                                "health"
                            ]
                        },
                        "description": "**Run this first.** It proves the whole chain at once — both headers, a live token, an allowed source address and a correct signature — which is far cheaper to discover here than on your first real order.\n\n`clock_skew_seconds` is our time minus your signed timestamp. **Positive means your clock is behind.** Past 300s every request starts failing as `signature_expired`, and that is very easy to misread as a wrong secret.\n\nA GET has no body, so it signs `\"{t}.\"` — the trailing dot is not a typo."
                    }
                },
                {
                    "name": "Push an order",
                    "event": [
                        {
                            "listen": "prerequest",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "// A fresh reference each run, so re-running does not trip the duplicate guard.",
                                    "pm.collectionVariables.set('external_order_id', 'SO-' + Date.now());"
                                ]
                            }
                        },
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "pm.test('created', () => pm.response.to.have.status(201));",
                                    "",
                                    "const model = pm.response.json().Model;",
                                    "pm.collectionVariables.set('order_uuid', model.uuid);",
                                    "",
                                    "pm.test('arrives pending, never pre-assigned', () => pm.expect(model.status).to.eql('pending'));"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{store_api_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"external_order_id\": \"{{external_order_id}}\",\n  \"external_order_number\": \"{{external_order_id}}\",\n  \"customer\": {\n    \"external_id\": \"CUST-1\",\n    \"name\": \"Customer\",\n    \"phone\": \"966500000000\"\n  },\n  \"pickup\": {\n    \"branch_ref\": \"BR-1\",\n    \"name\": \"Fresh Market — Olaya\",\n    \"address\": \"King Fahd Road\",\n    \"lat\": 24.7136,\n    \"lng\": 46.6753\n  },\n  \"dropoff\": {\n    \"address\": \"Al Murooj\",\n    \"lat\": 24.7520,\n    \"lng\": 46.6580\n  },\n  \"payment\": {\n    \"method\": \"cash_on_delivery\",\n    \"status\": \"unpaid\",\n    \"amount_to_collect\": 100\n  },\n  \"currency\": \"SAR\",\n  \"delivery_fee\": 12,\n  \"items\": [\n    { \"name\": \"Item\", \"quantity\": 1, \"unit_price\": 100 }\n  ]\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/integration/v1/orders",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "integration",
                                "v1",
                                "orders"
                            ]
                        },
                        "description": "Signed and given an `Idempotency-Key` automatically.\n\n**Five fields are required here that are optional on our own dashboard form**, each for a concrete reason:\n\n- `pickup.lat` / `lng` — without coordinates the order can never be assigned to anybody;\n- `dropoff.lat` / `lng` — no ETA, no geofence;\n- `customer.phone` — masked calling has nothing to dial;\n- `payment.status` — **the highest-severity field here.** Omit it and a prepaid order defaults to cash, and a captain collects money already paid;\n- `items` — at least one, or the item-mismatch flag is inert.\n\nSend the **same** `Idempotency-Key` twice and you get the first answer back verbatim with `Idempotency-Replayed: true`, not a second order. A retry is not an error.\n\nTo test the rotation overlap: after `01 → Rotate secrets`, temporarily set `signing_secret` to `{{previous_signing_secret}}` and run this again — still accepted, for 24 hours."
                    }
                },
                {
                    "name": "Read it back",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{store_api_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/integration/v1/orders/{{order_uuid}}",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "integration",
                                "v1",
                                "orders",
                                "{{order_uuid}}"
                            ]
                        },
                        "description": "Your repair path when a callback was missed. The body is byte-for-byte the shape every webhook carries, so one parser serves both.\n\nAnother store's order answers **404**, never 403 — a 403 would confirm it exists."
                    }
                },
                {
                    "name": "Edit the order",
                    "request": {
                        "method": "PATCH",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{store_api_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"customer\": {\n    \"phone\": \"966500000123\"\n  },\n  \"dropoff\": {\n    \"address\": \"Al Murooj, Building 12\",\n    \"lat\": 24.7520,\n    \"lng\": 46.6580\n  }\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/integration/v1/orders/{{order_uuid}}",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "integration",
                                "v1",
                                "orders",
                                "{{order_uuid}}"
                            ]
                        },
                        "description": "A **partial** edit. Anything you leave out is left alone; anything you send as `null` is cleared.\n\n**What can still change, and until when** — the rule is that an edit is refused once the thing it would change has already been acted on in the physical world:\n\n| Changing | Allowed until | Then |\n|---|---|---|\n| `customer.*`, `note` | the order ends | — |\n| `pickup.*` | `assigned` | 409 `pickup_locked` |\n| `items`, amounts | `picked_up` | 409 `order_already_collected` |\n| `dropoff.*` | `on_the_way` | 409 `order_in_transit` |\n\nTry it: advance the order to `on_the_way` and send a dropoff change. The captain is driving to the old address, so it is refused and the answer is cancel + re-push.\n\n**Never editable, answers 422 naming the field:** `payment.method` (a captain would collect money already paid), `external_order_id` (the duplicate key), `status`, `driver_uuid`.\n\n**`items` replaces, never merges.** Send the complete basket.\n\nAn empty body answers 422 `nothing_to_update` — a 200 would hide a client that built its payload wrongly."
                    }
                },
                {
                    "name": "Delete the order (does not exist)",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "pm.test('there is no DELETE', () => pm.response.to.have.status(405));"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "DELETE",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{store_api_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/integration/v1/orders/{{order_uuid}}",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "integration",
                                "v1",
                                "orders",
                                "{{order_uuid}}"
                            ]
                        },
                        "description": "**Expected to fail with 405, and it always will.**\n\nCancel is the delete. Destroying an order would take three things with it: the timeline a dispute is settled from, the delivery log entry proving we told you, and — worst — the release of the captain's reserved capacity. That last one leaves `drivers.active_orders` too high, which locks that captain out of *every* future assignment, silently, until somebody notices.\n\nIf your system deletes orders, map that to `POST /orders/{uuid}/cancel`."
                    }
                },
                {
                    "name": "Dispatcher: rank the captains",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/orders/{{order_uuid}}/captains",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "orders",
                                "{{order_uuid}}",
                                "captains"
                            ]
                        },
                        "description": "Not part of the portal, but the proof that a fed order is a **first-class** order: anything but 200 here would mean the intake produced something no dispatcher can act on.\n\n`422` almost always means the pickup had no coordinates. Copy a `driver_uuid` from the response into the `captain_uuid` variable for the next request."
                    }
                },
                {
                    "name": "Dispatcher: assign it",
                    "request": {
                        "method": "PATCH",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"driver_uuid\": \"{{captain_uuid}}\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/orders/{{order_uuid}}/assign",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "orders",
                                "{{order_uuid}}",
                                "assign"
                            ]
                        },
                        "description": "Fires the `order.assigned` callback. From here the captain app moves it on (`PATCH /api/driver/orders/{uuid}/picked-up`, `/on-the-way`, `/delivered`) — outside this collection, but each step adds a row you can watch in folder 05.\n\n`409` means the captain was taken, filled up or went off duty between the ranking and this confirmation."
                    }
                }
            ]
        },
        {
            "name": "05 — Portal: orders and callbacks",
            "description": "What the shop reads for itself. Everything here needs an **approved** store.\n\nNothing takes a store identifier: the tenant comes from the token, applied before every filter — so even a search that exactly matches another shop's order number returns nothing. Anything not yours is **404**.",
            "item": [
                {
                    "name": "My orders",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "const rows = pm.response.json().Model;",
                                    "",
                                    "if (Array.isArray(rows) && rows.length) {",
                                    "    pm.collectionVariables.set('order_uuid', rows[0].uuid);",
                                    "}"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/orders?rows=25&page=1",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "orders"
                            ],
                            "query": [
                                {
                                    "key": "rows",
                                    "value": "25"
                                },
                                {
                                    "key": "page",
                                    "value": "1"
                                },
                                {
                                    "key": "search",
                                    "value": "",
                                    "description": "Our number, your reference, or the customer name.",
                                    "disabled": true
                                },
                                {
                                    "key": "status",
                                    "value": "",
                                    "description": "pending | assigned | picked_up | on_the_way | delivered | delivery_failed | cancelled",
                                    "disabled": true
                                },
                                {
                                    "key": "payment_status",
                                    "value": "",
                                    "description": "paid | unpaid | refunded",
                                    "disabled": true
                                },
                                {
                                    "key": "external_order_id",
                                    "value": "",
                                    "description": "Your own reference, exactly.",
                                    "disabled": true
                                },
                                {
                                    "key": "from",
                                    "value": "",
                                    "description": "YYYY-MM-DD",
                                    "disabled": true
                                },
                                {
                                    "key": "to",
                                    "value": "",
                                    "description": "YYYY-MM-DD",
                                    "disabled": true
                                }
                            ]
                        }
                    }
                },
                {
                    "name": "One order, with its timeline",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/orders/{{order_uuid}}",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "orders",
                                "{{order_uuid}}"
                            ]
                        },
                        "description": "The `timeline` is the same transitions your servers are sent one at a time, gathered into a list.\n\nIt carries **no actor**: which of our staff or which captain moved an order is our operational record, and the webhook does not carry it either."
                    }
                },
                {
                    "name": "Did you ever tell us?",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "const rows = pm.response.json().Model;",
                                    "",
                                    "if (Array.isArray(rows) && rows.length) {",
                                    "    pm.collectionVariables.set('delivery_uuid', rows[0].uuid);",
                                    "}",
                                    "",
                                    "const summary = pm.response.json().Summary;",
                                    "",
                                    "if (summary && summary.pending > 0) {",
                                    "    console.warn('pending deliveries: ' + summary.pending + ' — is a queue worker running? (php artisan queue:work)');",
                                    "}"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/webhooks?rows=25&page=1",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "webhooks"
                            ],
                            "query": [
                                {
                                    "key": "rows",
                                    "value": "25"
                                },
                                {
                                    "key": "page",
                                    "value": "1"
                                },
                                {
                                    "key": "status",
                                    "value": "",
                                    "description": "pending | sent | failed | dropped",
                                    "disabled": true
                                },
                                {
                                    "key": "event",
                                    "value": "",
                                    "description": "e.g. order.delivered",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "The commonest support question this integration produces, answerable by the shop itself.\n\n`Summary` counts your deliveries by status:\n\n- **pending** piling up → no queue worker is running on our side;\n- **dropped** → we gave up after six attempts and a human has to look;\n- **failed** → retrying, `next_attempt_at` says when."
                    }
                },
                {
                    "name": "One delivery",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/webhooks/{{delivery_uuid}}",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "webhooks",
                                "{{delivery_uuid}}"
                            ]
                        },
                        "description": "The payload we sent, the status your endpoint answered, and the error if there was one."
                    }
                },
                {
                    "name": "Send it again",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/webhooks/{{delivery_uuid}}/replay",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "webhooks",
                                "{{delivery_uuid}}",
                                "replay"
                            ]
                        },
                        "description": "Resets the attempt count and queues it afresh. Owner only.\n\nSafe to expose to a shop because the destination is not theirs to choose — it is the URL already on their own record. But your handler **will** see the event a second time, which is what `event_id` is for."
                    }
                }
            ]
        },
        {
            "name": "06 — Portal: payments",
            "description": "Two halves, and both are needed for \"reconciliation\" to mean anything: what the period **came to**, and what was actually **paid**.",
            "item": [
                {
                    "name": "Statement",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "const model = pm.response.json().Model;",
                                    "",
                                    "pm.test('the figures carry their assumptions', function () {",
                                    "    pm.expect(model.assumptions).to.be.an('array').that.is.not.empty;",
                                    "});",
                                    "",
                                    "(model.assumptions || []).forEach(a => console.log('assumption: ' + a));"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/payments/statement",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "payments",
                                "statement"
                            ],
                            "query": [
                                {
                                    "key": "from",
                                    "value": "",
                                    "description": "YYYY-MM-DD. Defaults to 30 days back.",
                                    "disabled": true
                                },
                                {
                                    "key": "to",
                                    "value": "",
                                    "description": "YYYY-MM-DD. Defaults to today.",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "Derived from delivered orders. A range entered backwards is swapped rather than answered empty — \"you delivered nothing\" is a very different claim from \"those dates are the wrong way round\".\n\n**Read `assumptions` and show it on the screen.** The important one: nothing records a captain handing over an amount different from the one due, so a delivered cash order is *assumed* to have yielded exactly `amount_to_collect`. Quoting a cash figure in a dispute without saying that is how an argument starts.\n\n- `totals` — **one row per currency**, never summed across them.\n- `outcomes` — failed and cancelled counted **beside** the money, never netted into it.\n- `settled_to_date` — what we have actually paid. The bridge to the next request."
                    }
                },
                {
                    "name": "The orders behind a total",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/payments/orders?rows=25&page=1",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "payments",
                                "orders"
                            ],
                            "query": [
                                {
                                    "key": "rows",
                                    "value": "25"
                                },
                                {
                                    "key": "page",
                                    "value": "1"
                                },
                                {
                                    "key": "from",
                                    "value": "",
                                    "disabled": true
                                },
                                {
                                    "key": "to",
                                    "value": "",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "So a disputed figure resolves to the lines that produced it, rather than to an argument about a total."
                    }
                },
                {
                    "name": "What we have paid you",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/payments/settlements?rows=25&page=1",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "payments",
                                "settlements"
                            ],
                            "query": [
                                {
                                    "key": "rows",
                                    "value": "25"
                                },
                                {
                                    "key": "page",
                                    "value": "1"
                                }
                            ]
                        },
                        "description": "Settlements still being drawn up are **not shown** — a figure nobody has stood behind yet is not one worth arguing about. A draft answers 404 here too, not 403."
                    }
                },
                {
                    "name": "One settlement",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/payments/settlements/{{settlement_uuid}}",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "payments",
                                "settlements",
                                "{{settlement_uuid}}"
                            ]
                        }
                    }
                }
            ]
        },
        {
            "name": "07 — Back office: settlements",
            "description": "Recording what actually changed hands.\n\nThe statement is derived from the orders, so on its own it can only ever agree with itself. These rows are the other half, and the gap between the two is the conversation.\n\n`settlements.view` reads the ledger; `settlements.manage` moves money in it.",
            "item": [
                {
                    "name": "Preview a period",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/store-clients/{{store_uuid}}/settlements/preview?from=2026-09-01&to=2026-09-30",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "store-clients",
                                "{{store_uuid}}",
                                "settlements",
                                "preview"
                            ],
                            "query": [
                                {
                                    "key": "from",
                                    "value": "2026-09-01"
                                },
                                {
                                    "key": "to",
                                    "value": "2026-09-30"
                                }
                            ]
                        },
                        "description": "**Always run this before drawing a payment up.** It writes nothing; the point is that the figures reach a human before they become a payment, instead of being retyped off another screen — which is where a transposed digit gets into a transfer.\n\nBoth dates are required here, unlike the store's own statement: a payout covers a stated period, and defaulting one silently would put \"the last thirty days from whenever you clicked\" into a financial record."
                    }
                },
                {
                    "name": "Draw one up (draft)",
                    "event": [
                        {
                            "listen": "test",
                            "script": {
                                "type": "text/javascript",
                                "exec": [
                                    "if (pm.response.code !== 201) { return; }",
                                    "",
                                    "const model = pm.response.json().Model;",
                                    "pm.collectionVariables.set('settlement_uuid', model.uuid);",
                                    "",
                                    "pm.test('born a draft', () => pm.expect(model.status).to.eql('draft'));"
                                ]
                            }
                        }
                    ],
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"period_start\": \"2026-09-01\",\n  \"period_end\": \"2026-09-30\",\n  \"currency\": \"SAR\",\n  \"cash_collected\": 100,\n  \"fees_charged\": 12,\n  \"net_amount\": 88,\n  \"orders_count\": 1,\n  \"reference\": \"TRF-0001\",\n  \"note\": \"September\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/store-clients/{{store_uuid}}/settlements",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "store-clients",
                                "{{store_uuid}}",
                                "settlements"
                            ]
                        },
                        "description": "**A draft is invisible to the store.** They see it once somebody has stood behind it.\n\nThe amounts you send are recorded as sent — they are allowed to differ from the preview. An adjustment, a rounding, a dispute settled halfway: what is recorded is what a human agreed to pay, not what the orders imply.\n\n`net_amount` may be **negative**: a mostly-prepaid month where our fees exceed the cash collected leaves the shop owing us, and a ledger that could only express one direction would force somebody to fudge it."
                    }
                },
                {
                    "name": "Correct a draft",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"period_start\": \"2026-09-01\",\n  \"period_end\": \"2026-09-30\",\n  \"currency\": \"SAR\",\n  \"cash_collected\": 100,\n  \"fees_charged\": 15,\n  \"net_amount\": 85,\n  \"orders_count\": 1,\n  \"reference\": \"TRF-0001\",\n  \"note\": \"September, fee corrected\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/settlements/{{settlement_uuid}}",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "settlements",
                                "{{settlement_uuid}}"
                            ]
                        },
                        "description": "Only while it is a draft. A settled one answers `409 settlement_not_editable` — the way to correct a payment both sides are holding is **another payment**, not a rewrite of the one they agreed."
                    }
                },
                {
                    "name": "Mark it paid",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/settlements/{{settlement_uuid}}/settle",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "settlements",
                                "{{settlement_uuid}}",
                                "settle"
                            ]
                        },
                        "description": "From here the store can see it and neither side may edit it.\n\nThe figures are **frozen**, not recomputed: if an order is corrected next month, a payment already made must not change underneath the people holding it."
                    }
                },
                {
                    "name": "Cancel a draft",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/settlements/{{settlement_uuid}}/cancel",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "settlements",
                                "{{settlement_uuid}}",
                                "cancel"
                            ]
                        }
                    }
                },
                {
                    "name": "The ledger for one store",
                    "request": {
                        "method": "GET",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{admin_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/dashboard/store-clients/{{store_uuid}}/settlements?rows=25&page=1",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "dashboard",
                                "store-clients",
                                "{{store_uuid}}",
                                "settlements"
                            ],
                            "query": [
                                {
                                    "key": "rows",
                                    "value": "25"
                                },
                                {
                                    "key": "page",
                                    "value": "1"
                                },
                                {
                                    "key": "status",
                                    "value": "",
                                    "description": "draft | settled | cancelled",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "Drafts included, unlike the store's own view."
                    }
                }
            ]
        },
        {
            "name": "08 — Account and recovery",
            "description": "The signed-in person's own record, and the way back in when a password is lost.\n\nAlso how an **invited colleague** signs in for the first time: they have no password, so their first act is to ask for a code here.",
            "item": [
                {
                    "name": "Update my details",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"name\": \"Layla Qassim\",\n  \"phone\": \"966500000001\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/profile",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "profile"
                            ]
                        },
                        "description": "Not the email — it is the sign-in identifier, and an editable one makes a stolen session permanent. Not the role either: a staff account that could promote itself would be an owner account."
                    }
                },
                {
                    "name": "Change my password",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            },
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"current_password\": \"{{store_password}}\",\n  \"password\": \"a-better-password-12\",\n  \"password_confirmation\": \"a-better-password-12\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/profile/password",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "profile",
                                "password"
                            ]
                        },
                        "description": "Leaves your other sessions alone — nothing here suggests the old password leaked, and revoking the token this very request authenticated with would sign you out of the screen you are typing on.\n\nA **recovery** does the opposite; see below.\n\nRemember to update the `store_password` variable if you run this."
                    }
                },
                {
                    "name": "Forgot password",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"email\": \"{{store_email}}\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/auth/forgot-password",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "auth",
                                "forgot-password"
                            ]
                        },
                        "description": "Sends a code to the phone on the account. Answers `200` for an address nobody holds, and sends nothing.\n\n**Locally the code is written to the log, not sent** — `storage/logs/laravel.log`. Copy it into the `otp_code` variable.\n\nThis is also how an invited colleague sets their first password."
                    }
                },
                {
                    "name": "Resend the code",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"email\": \"{{store_email}}\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/auth/resend-code",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "auth",
                                "resend-code"
                            ]
                        },
                        "description": "Replaces the pending code — only the newest one is accepted."
                    }
                },
                {
                    "name": "Reset the password",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"email\": \"{{store_email}}\",\n  \"code\": \"{{otp_code}}\",\n  \"password\": \"{{store_password}}\",\n  \"password_confirmation\": \"{{store_password}}\"\n}"
                        },
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/auth/reset-password",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "auth",
                                "reset-password"
                            ]
                        },
                        "description": "**Every other session is revoked.** A recovery is what somebody does when they have lost control of an account, so anything already signed in with it is exactly what should stop working.\n\nAfter this, sign in again — your `portal_token` is dead."
                    }
                },
                {
                    "name": "Sign out",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Authorization",
                                "value": "Bearer {{portal_token}}"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/store-portal/auth/logout",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "store-portal",
                                "auth",
                                "logout"
                            ]
                        },
                        "description": "Revokes every token this person holds, not just this one."
                    }
                }
            ]
        }
    ]
}
