{
  "info": {
    "_postman_id": "b73d9f1d-6a96-494b-b78c-715a25857904",
    "name": "Waslni Shop API",
    "description": "Auto-signing collection for the Waslni Shop External API.\n\n**How to use**\n1. Open the collection → *Variables* tab.\n2. Paste your `publicKey` and `secretKey` (generated in your shop owner app → API Keys).\n3. Run *Create Order* — the new `orderId` is captured automatically and reused by the other requests.\n\nThe pre-request script signs every call using the same HMAC-SHA256 scheme as the live API. Do **not** commit this file with real secrets.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Waslni Shop API — request signing.",
          "// Matches backend/middlewares/CheckApiKey.js:",
          "//   hmacKey      = sha256(secretKey)                              // hex",
          "//   stringToSign = ts + \"\\n\" + METHOD + \"\\n\" + path + \"\\n\" + sha256(body)",
          "//   signature    = HMAC_SHA256(hmacKey, stringToSign)             // hex",
          "// Path includes the query string. Body is the literal raw request body",
          "// (empty string for GETs).",
          "",
          "const publicKey = pm.collectionVariables.get('publicKey');",
          "const secretKey = pm.collectionVariables.get('secretKey');",
          "",
          "if (!publicKey || !secretKey || /YOUR_/i.test(publicKey) || /YOUR_/i.test(secretKey)) {",
          "    throw new Error('Set publicKey and secretKey in the collection variables before sending.');",
          "}",
          "",
          "// Generate a fresh idempotency key on each send and stash it on a collection",
          "// variable so the request body / header reference {{_idemKey}} sees the SAME",
          "// value the script signs (Postman's {{$randomUUID}} re-evaluates per access",
          "// and would mismatch the signature).",
          "const idemKey = CryptoJS.lib.WordArray.random(16).toString(CryptoJS.enc.Hex);",
          "pm.collectionVariables.set('_idemKey', idemKey);",
          "",
          "// Resolve any {{...}} references in path / body so we sign exactly what gets sent.",
          "let path = pm.request.url.getPathWithQuery();",
          "path = pm.variables.replaceIn(path);",
          "if (!path.startsWith('/')) path = '/' + path;",
          "",
          "const method = pm.request.method.toUpperCase();",
          "let body = '';",
          "if (pm.request.body && pm.request.body.raw) {",
          "    body = pm.variables.replaceIn(pm.request.body.raw);",
          "}",
          "",
          "const ts = Math.floor(Date.now() / 1000).toString();",
          "const hmacKey = CryptoJS.SHA256(secretKey).toString(CryptoJS.enc.Hex);",
          "const bodyHash = CryptoJS.SHA256(body).toString(CryptoJS.enc.Hex);",
          "const stringToSign = ts + '\\n' + method + '\\n' + path + '\\n' + bodyHash;",
          "const signature = CryptoJS.HmacSHA256(stringToSign, hmacKey).toString(CryptoJS.enc.Hex);",
          "",
          "pm.request.headers.upsert({ key: 'x-api-key',   value: publicKey });",
          "pm.request.headers.upsert({ key: 'x-timestamp', value: ts });",
          "pm.request.headers.upsert({ key: 'x-signature', value: signature });"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "host",
      "value": "https://api.example.com",
      "type": "string"
    },
    {
      "key": "publicKey",
      "value": "YOUR_PUBLIC_KEY_HERE",
      "type": "string"
    },
    {
      "key": "secretKey",
      "value": "YOUR_SECRET_KEY_HERE",
      "type": "string"
    },
    {
      "key": "orderId",
      "value": "",
      "type": "string"
    },
    {
      "key": "_idemKey",
      "value": "",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "0. Get Price Quote (no order created)",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{host}}/api/v2/external/orders/quote",
          "host": [
            "{{host}}"
          ],
          "path": [
            "api",
            "v2",
            "external",
            "orders",
            "quote"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"deliveryAddress\": {\n    \"lat\": 31.2001,\n    \"lng\": 29.9187,\n    \"address\": \"شارع صفية زغلول، الإسكندرية، مصر\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "Returns the delivery fee, distance, ETA, currency, and a single-use `quoteId` (with `quoteExpiresAt` + `ttlSeconds`). Pass the `quoteId` to *Create Order* within the validity window (default 5 min) to lock the fee — protects against pricing-config changes between checkout and order creation. Rate-limited to 1 request/second per API key."
      },
      "response": []
    },
    {
      "name": "1. Create Order (Alexandria, EG)",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "x-idempotency-key",
            "value": "{{_idemKey}}"
          }
        ],
        "url": {
          "raw": "{{host}}/api/v2/external/orders",
          "host": [
            "{{host}}"
          ],
          "path": [
            "api",
            "v2",
            "external",
            "orders"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"customerName\": \"Ahmed Ali\",\n  \"customerPhone\": \"01001234567\",\n  \"deliveryAddress\": {\n    \"lat\": 31.2001,\n    \"lng\": 29.9187,\n    \"address\": \"شارع صفية زغلول، الإسكندرية، مصر\"\n  },\n  \"packageNote\": \"2 boxes, fragile\",\n  \"deliveryNotes\": \"Call on arrival\",\n  \"prepaidAmount\": 0,\n  \"referenceId\": \"YOUR-ORDER-1\",\n  \"serviceTypeId\": null\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "Creates a delivery order. The `x-idempotency-key` header lets you safely retry on network errors — replays return the original 201 with the `x-idempotent-replayed: true` response header.\n\n**Optional fields**:\n- `quoteId` — the handle returned by *Get Price Quote* to lock the delivery fee.\n- `estimatedPrepMinutes` — kitchen prep time (see *Create Order with Prep Delay*).\n- `serviceTypeId` — pin the dispatch to a specific courier-eligible vehicle class (e.g. car for bulky packages). null/omit = any courier-eligible driver. Discover ids via `GET /api/v2/service-types`.\n\nResponse always carries `currency` (per-tenant ISO 4217 code)."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));",
              "const json = pm.response.json();",
              "if (json && json.order && json.order.id) {",
              "    pm.collectionVariables.set('orderId', String(json.order.id));",
              "    console.log('Saved orderId:', json.order.id);",
              "}",
              "pm.test('Returned order id', () => pm.expect(json.order && json.order.id).to.exist);"
            ]
          }
        }
      ]
    },
    {
      "name": "1b. Create Order with Prep Delay (restaurant flow)",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "x-idempotency-key",
            "value": "{{_idemKey}}"
          }
        ],
        "url": {
          "raw": "{{host}}/api/v2/external/orders",
          "host": [
            "{{host}}"
          ],
          "path": [
            "api",
            "v2",
            "external",
            "orders"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"customerName\": \"Ahmed Ali\",\n  \"customerPhone\": \"01001234567\",\n  \"deliveryAddress\": {\n    \"lat\": 31.2001,\n    \"lng\": 29.9187,\n    \"address\": \"شارع صفية زغلول، الإسكندرية، مصر\"\n  },\n  \"packageNote\": \"2 pizzas, ready in ~25 min\",\n  \"deliveryNotes\": \"Call on arrival\",\n  \"prepaidAmount\": 0,\n  \"referenceId\": \"YOUR-ORDER-PREP-1\",\n  \"serviceTypeId\": null,\n  \"estimatedPrepMinutes\": 25\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "Creates a delivery order in `preparing` status with a `prepReadyAt` timestamp. Drivers are kept hidden from this order until `prepReadyAt − leadMinutes` (default lead = 20 min, admin-tunable per tenant). At that point drivers see the offer with a \"Ready in ~X min\" badge so they can plan to arrive when the food is actually ready.\n\nUse this for restaurant integrations where the kitchen needs prep time. Submissions above the cap (default 180 min) are rejected with `400 VALIDATION_ERROR` carrying the cap value."
      },
      "response": []
    },
    {
      "name": "2. List Orders",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{host}}/api/v2/external/orders?limit=20&page=1&status=&createdBy=api",
          "host": [
            "{{host}}"
          ],
          "path": [
            "api",
            "v2",
            "external",
            "orders"
          ],
          "query": [
            {
              "key": "limit",
              "value": "20"
            },
            {
              "key": "page",
              "value": "1"
            },
            {
              "key": "status",
              "value": "",
              "disabled": true
            },
            {
              "key": "createdBy",
              "value": "api",
              "disabled": true
            }
          ]
        },
        "description": "List the shop's orders. Filter by status / createdBy / from / to."
      },
      "response": []
    },
    {
      "name": "3. Get Order Detail",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{host}}/api/v2/external/orders/{{orderId}}",
          "host": [
            "{{host}}"
          ],
          "path": [
            "api",
            "v2",
            "external",
            "orders",
            "{{orderId}}"
          ]
        },
        "description": "Full order detail including driver info and timeline. `orderId` is auto-set by the *Create Order* test script."
      },
      "response": []
    },
    {
      "name": "4. Update Delivery Address",
      "request": {
        "method": "PATCH",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{host}}/api/v2/external/orders/{{orderId}}/address",
          "host": [
            "{{host}}"
          ],
          "path": [
            "api",
            "v2",
            "external",
            "orders",
            "{{orderId}}",
            "address"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"deliveryAddress\": {\n    \"lat\": 31.2156,\n    \"lng\": 29.9553,\n    \"address\": \"محطة الرمل، الإسكندرية، مصر\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "Change the delivery address before pickup. Delivery fee is recalculated server-side."
      },
      "response": []
    },
    {
      "name": "5. Cancel Order",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{host}}/api/v2/external/orders/{{orderId}}/cancel",
          "host": [
            "{{host}}"
          ],
          "path": [
            "api",
            "v2",
            "external",
            "orders",
            "{{orderId}}",
            "cancel"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"reason\": \"Customer changed mind\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "Cancel an order before the driver picks it up. Returns `422 ORDER_NOT_CANCELLABLE` if status >= picked_up. Any pending prep-dispatch jobs are cancelled too — no late driver pings."
      },
      "response": []
    },
    {
      "name": "6. Mark Order Ready Now (release prep early)",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{host}}/api/v2/external/orders/{{orderId}}/ready",
          "host": [
            "{{host}}"
          ],
          "path": [
            "api",
            "v2",
            "external",
            "orders",
            "{{orderId}}",
            "ready"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "Lets the integrator release a `preparing` order ahead of schedule (e.g. food is actually ready in 12 min, not 25). Cancels both pending prep-dispatch jobs, flips status to `ready_for_pickup`, and dispatches drivers inline if the lead window had not yet opened. Idempotent: returns 200 with current state if the order is already ready or beyond. Returns `422 ORDER_CANCELLED` for cancelled orders."
      },
      "response": []
    }
  ]
}