{
  "info": {
    "name": "eNadawca API",
    "description": "RESTful API do generowania numerów przesyłek pocztowych i kurierskich Poczty Polskiej.\n\n## Autoryzacja\nKolekcja automatycznie pobiera token OAuth 2.0 (client_credentials) przed każdym chronionym requestem.\n\n## Zmienne\nUstaw zmienne w zakładce **Variables** lub importuj plik środowiska (*.postman_environment.json):\n- `base_url` — adres API\n- `client_id` — Keycloak client ID\n- `client_secret` — Keycloak client secret\n- `keycloak_url` — token endpoint Keycloak\n\nv2.1 — dodano: Auth Token proxy, Admin Access Requests (lista, szczegół, approve, reject).",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "_postman_id": "enadawca-api-v1"
  },
  "variable": [
    {
      "key": "base_url",
      "value": "http://localhost:8000",
      "description": "Adres bazowy API (bez trailing slash)",
      "type": "string"
    },
    {
      "key": "client_id",
      "value": "ppsa_api_en",
      "description": "Keycloak client_id (service account)",
      "type": "string"
    },
    {
      "key": "client_secret",
      "value": "",
      "description": "Keycloak client_secret — uzupełnij przed użyciem",
      "type": "string"
    },
    {
      "key": "keycloak_url",
      "value": "https://dev.sniezek.eu/login/realms/ppsa/protocol/openid-connect/token",
      "description": "Keycloak token endpoint",
      "type": "string"
    },
    {
      "key": "access_token",
      "value": "",
      "description": "Bearer token — uzupełniany automatycznie przez Pre-request Script",
      "type": "string"
    },
    {
      "key": "token_expires_at",
      "value": "0",
      "description": "Timestamp wygaśnięcia tokenu (epoch seconds)",
      "type": "string"
    },
    {
      "key": "approved_client_id",
      "value": "",
      "type": "string",
      "description": "Client ID zatwierdzony po approve"
    },
    {
      "key": "last_tracking_number",
      "value": "",
      "description": "Ostatnio nadany numer śledzenia",
      "type": "string"
    }
  ],
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{access_token}}",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Auto-fetch Keycloak token when missing or expired",
          "const token = pm.collectionVariables.get('access_token');",
          "const expiresAt = parseInt(pm.collectionVariables.get('token_expires_at') || '0');",
          "const now = Math.floor(Date.now() / 1000);",
          "",
          "if (!token || now >= expiresAt - 30) {",
          "    const clientId = pm.collectionVariables.get('client_id');",
          "    const clientSecret = pm.collectionVariables.get('client_secret');",
          "    const keycloakUrl = pm.collectionVariables.get('keycloak_url');",
          "",
          "    if (!clientId || !clientSecret || !keycloakUrl) {",
          "        console.warn('eNadawca: Ustaw zmienne client_id, client_secret i keycloak_url');",
          "        return;",
          "    }",
          "",
          "    pm.sendRequest({",
          "        url: keycloakUrl,",
          "        method: 'POST',",
          "        header: { 'Content-Type': 'application/x-www-form-urlencoded' },",
          "        body: {",
          "            mode: 'urlencoded',",
          "            urlencoded: [",
          "                { key: 'grant_type', value: 'client_credentials' },",
          "                { key: 'client_id', value: clientId },",
          "                { key: 'client_secret', value: clientSecret }",
          "            ]",
          "        }",
          "    }, function (err, res) {",
          "        if (err) {",
          "            console.error('eNadawca: Błąd pobierania tokenu:', err);",
          "            return;",
          "        }",
          "        const json = res.json();",
          "        if (json.access_token) {",
          "            pm.collectionVariables.set('access_token', json.access_token);",
          "            pm.collectionVariables.set('token_expires_at', String(Math.floor(Date.now() / 1000) + (json.expires_in || 300)));",
          "            console.log('eNadawca: Token pobrany, wygasa za', json.expires_in, 'sekund');",
          "        } else {",
          "            console.error('eNadawca: Brak access_token w odpowiedzi:', json);",
          "        }",
          "    });",
          "}"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Globalny test: odpowiedź musi być JSON",
          "if (pm.response.headers.get('Content-Type') && pm.response.headers.get('Content-Type').includes('application/json')) {",
          "    pm.test('Response is valid JSON', function () {",
          "        pm.response.to.be.json;",
          "    });",
          "}"
        ]
      }
    }
  ],
  "item": [
    {
      "name": "Health",
      "item": [
        {
          "name": "GET /api/v1/health",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/api/v1/health",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "health"
              ]
            },
            "description": "Sprawdzenie dostępności serwisu. Endpoint publiczny — nie wymaga autoryzacji."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "pm.test('Status ok', () => {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json.data.status).to.eql('ok');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Products",
      "item": [
        {
          "name": "GET /api/v1/products",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/api/v1/products",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "products"
              ],
              "query": [
                {
                  "key": "category",
                  "value": "",
                  "description": "Filtr kategorii: letter, parcel, courier, international, special",
                  "disabled": true
                }
              ]
            },
            "description": "Lista aktywnych produktów (typów przesyłek). Endpoint publiczny."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "pm.test('Has data array', () => {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json.data).to.be.an('array');",
                  "    pm.expect(json.meta.total).to.be.a('number');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /api/v1/products/:id",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/api/v1/products/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "products",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "UUID produktu"
                }
              ]
            },
            "description": "Szczegóły produktu po ID."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200 or 404', () => {",
                  "    pm.expect(pm.response.code).to.be.oneOf([200, 404]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /api/v1/products",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/api/v1/products",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "products"
              ]
            },
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"KURIER_TEST\",\n  \"name\": \"Kurier testowy\",\n  \"category\": \"courier\",\n  \"type\": \"courier\",\n  \"description\": \"Produkt testowy\",\n  \"maxWeightGrams\": 31500,\n  \"maxLengthCm\": 120,\n  \"maxWidthCm\": 80,\n  \"maxHeightCm\": 60,\n  \"pricingStrategy\": \"courier_standard\",\n  \"availableServices\": [\"tracking\", \"confirmation\"],\n  \"isActive\": true,\n  \"sortOrder\": 999\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Tworzenie nowego produktu. Wymaga roli ROLE_ADMIN."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 201', () => pm.response.to.have.status(201));",
                  "pm.test('Has product id', () => {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json.data.id).to.be.a('string');",
                  "    pm.collectionVariables.set('last_product_id', json.data.id);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "PUT /api/v1/products/:id",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/api/v1/products/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "products",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{last_product_id}}",
                  "description": "UUID produktu"
                }
              ]
            },
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"KURIER_TEST\",\n  \"name\": \"Kurier testowy (zaktualizowany)\",\n  \"category\": \"courier\",\n  \"type\": \"courier\",\n  \"maxWeightGrams\": 31500,\n  \"maxLengthCm\": 120,\n  \"maxWidthCm\": 80,\n  \"maxHeightCm\": 60,\n  \"pricingStrategy\": \"courier_standard\",\n  \"availableServices\": [\"tracking\"],\n  \"isActive\": true,\n  \"sortOrder\": 999\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Aktualizacja produktu. Wymaga roli ROLE_ADMIN."
          }
        },
        {
          "name": "PATCH /api/v1/products/:id/toggle",
          "request": {
            "method": "PATCH",
            "url": {
              "raw": "{{base_url}}/api/v1/products/:id/toggle",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "products",
                ":id",
                "toggle"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{last_product_id}}",
                  "description": "UUID produktu"
                }
              ]
            },
            "description": "Przełączenie aktywności produktu (aktywny ↔ nieaktywny). Wymaga roli ROLE_ADMIN."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Shipments",
      "item": [
        {
          "name": "POST /api/v1/shipments",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/api/v1/shipments",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "shipments"
              ]
            },
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"productId\": \"{{last_product_id}}\",\n  \"senderName\": \"Jan Kowalski\",\n  \"senderStreet\": \"ul. Polna 1\",\n  \"senderCity\": \"Warszawa\",\n  \"senderPostalCode\": \"00-001\",\n  \"senderCountry\": \"PL\",\n  \"senderPhone\": \"+48500000000\",\n  \"senderEmail\": \"nadawca@example.com\",\n  \"recipientName\": \"Anna Nowak\",\n  \"recipientStreet\": \"ul. Kwiatowa 5\",\n  \"recipientCity\": \"Kraków\",\n  \"recipientPostalCode\": \"30-001\",\n  \"recipientCountry\": \"PL\",\n  \"recipientPhone\": \"+48600000000\",\n  \"recipientEmail\": \"odbiorca@example.com\",\n  \"weightGrams\": 1500,\n  \"lengthCm\": 30,\n  \"widthCm\": 20,\n  \"heightCm\": 15,\n  \"declaredValueCents\": 10000,\n  \"currency\": \"PLN\",\n  \"services\": [\"tracking\"],\n  \"notes\": \"Ostrożnie — szkło\",\n  \"referenceNumber\": \"REF-2026-001\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Tworzenie nowej przesyłki. Wymaga roli ROLE_CLIENT."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 201', () => pm.response.to.have.status(201));",
                  "pm.test('Has shipment id', () => {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json.data.id).to.be.a('string');",
                  "    pm.collectionVariables.set('last_shipment_id', json.data.id);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /api/v1/shipments",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/api/v1/shipments?page=1&limit=25",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "shipments"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "limit",
                  "value": "25"
                },
                {
                  "key": "status",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "productId",
                  "value": "",
                  "disabled": true
                }
              ]
            },
            "description": "Lista przesyłek klienta z paginacją."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "pm.test('Pagination meta', () => {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json.meta).to.have.property('page');",
                  "    pm.expect(json.meta).to.have.property('total');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /api/v1/shipments/:id",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/api/v1/shipments/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "shipments",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{last_shipment_id}}",
                  "description": "UUID przesyłki"
                }
              ]
            },
            "description": "Szczegóły przesyłki."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200 or 404', () => {",
                  "    pm.expect(pm.response.code).to.be.oneOf([200, 404]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "DELETE /api/v1/shipments/:id",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{base_url}}/api/v1/shipments/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "shipments",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{last_shipment_id}}",
                  "description": "UUID przesyłki (tylko status DRAFT)"
                }
              ]
            },
            "description": "Soft delete przesyłki — tylko ze statusem DRAFT."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 204 or 422', () => {",
                  "    pm.expect(pm.response.code).to.be.oneOf([204, 422, 404]);",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Orders",
      "item": [
        {
          "name": "List Orders",
          "request": {
            "method": "GET",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/api/v1/orders?page=1&limit=25",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "orders"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "draft",
                  "disabled": true
                },
                {
                  "key": "date_from",
                  "value": "2026-01-01",
                  "disabled": true
                },
                {
                  "key": "date_to",
                  "value": "2026-12-31",
                  "disabled": true
                },
                {
                  "key": "reference_number",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "limit",
                  "value": "25"
                }
              ]
            }
          }
        },
        {
          "name": "Get Order",
          "request": {
            "method": "GET",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/api/v1/orders/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "orders",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{order_id}}"
                }
              ]
            }
          }
        },
        {
          "name": "Create Order (from existing shipments)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/api/v1/orders",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "orders"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"shipment_ids\": [\n    \"{{shipment_id}}\"\n  ],\n  \"currency\": \"PLN\",\n  \"notes\": \"Zam\\u00f3wienie testowe\",\n  \"reference_number\": \"REF-001\"\n}"
            }
          }
        },
        {
          "name": "Confirm Order (pricing + tracking numbers)",
          "request": {
            "method": "POST",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/api/v1/orders/:id/confirm",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "orders",
                ":id",
                "confirm"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{order_id}}"
                }
              ]
            },
            "description": "Calculates prices for all shipments and assigns tracking numbers atomically."
          }
        },
        {
          "name": "Cancel Order",
          "request": {
            "method": "DELETE",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/api/v1/orders/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "orders",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{order_id}}"
                }
              ]
            }
          }
        },
        {
          "name": "Quick Order (all-in-one)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/api/v1/orders/quick",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "orders",
                "quick"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"shipments\": [\n    {\n      \"productId\": \"{{product_id}}\",\n      \"senderName\": \"Jan Kowalski\",\n      \"senderStreet\": \"ul. Testowa 1\",\n      \"senderCity\": \"Warszawa\",\n      \"senderPostalCode\": \"00-001\",\n      \"recipientName\": \"Anna Nowak\",\n      \"recipientStreet\": \"ul. G\\u0142\\u00f3wna 5\",\n      \"recipientCity\": \"Krak\\u00f3w\",\n      \"recipientPostalCode\": \"30-001\",\n      \"weightGrams\": 500,\n      \"services\": [\n        \"tracking\"\n      ]\n    }\n  ],\n  \"currency\": \"PLN\",\n  \"notes\": \"Zam\\u00f3wienie quick \\u2014 wszystko w jednym reque\\u015bcie\",\n  \"reference_number\": \"QUICK-001\"\n}"
            },
            "description": "Creates shipments, calculates prices, and assigns tracking numbers in a single atomic transaction."
          }
        }
      ]
    },
    {
      "name": "Tracking Numbers",
      "description": "Generowanie i wyszukiwanie numerów nadania (UPU S10).",
      "item": [
        {
          "name": "POST /api/v1/shipments/{id}/assign-number",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/api/v1/shipments/{{last_shipment_id}}/assign-number",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "shipments",
                "{{last_shipment_id}}",
                "assign-number"
              ]
            },
            "description": "Nadaje unikalny numer nadania (UPU S10) przesyłce w statusie CREATED. Wymaga scope shipments:write."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "pm.test('Has tracking_number', () => {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json.data.tracking_number).to.be.a('string');",
                  "    pm.collectionVariables.set('last_tracking_number', json.data.tracking_number);",
                  "});",
                  "pm.test('Status is number_assigned', () => {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json.data.status).to.equal('number_assigned');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "POST /api/v1/shipments/assign-numbers (batch)",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/api/v1/shipments/assign-numbers",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "shipments",
                "assign-numbers"
              ]
            },
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"shipment_ids\": [\n    \"{{last_shipment_id}}\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Hurtowe nadawanie numerów dla max 100 przesyłek w jednym requeście. Wszystkie muszą być w statusie CREATED."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "pm.test('assigned count matches', () => {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json.data.assigned).to.be.a('number');",
                  "    pm.expect(json.data.results).to.be.an('array');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /api/v1/tracking/{number}",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/api/v1/tracking/{{last_tracking_number}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "tracking",
                "{{last_tracking_number}}"
              ]
            },
            "auth": {
              "type": "noauth"
            },
            "description": "Wyszukaj przesyłkę po numerze nadania. Endpoint publiczny — bez tokenu."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "pm.test('Has tracking data', () => {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json.data.tracking_number).to.be.a('string');",
                  "    pm.expect(json.data.status).to.be.a('string');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Admin — Users",
      "item": [
        {
          "name": "GET /api/v1/admin/users",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/api/v1/admin/users?page=1&limit=25",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "users"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "limit",
                  "value": "25"
                },
                {
                  "key": "search",
                  "value": "",
                  "disabled": true
                }
              ]
            },
            "description": "Lista użytkowników z Keycloak. Wymaga roli ROLE_ADMIN."
          }
        },
        {
          "name": "POST /api/v1/admin/users",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/api/v1/admin/users",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "users"
              ]
            },
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"username\": \"testuser\",\n  \"email\": \"test@example.com\",\n  \"firstName\": \"Test\",\n  \"lastName\": \"User\",\n  \"password\": \"Password123!\",\n  \"role\": \"client\",\n  \"enabled\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Tworzenie użytkownika w Keycloak. Wymaga roli ROLE_ADMIN.",
            "event": [
              {
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "if (pm.response.code === 201) {",
                    "    const json = pm.response.json();",
                    "    pm.collectionVariables.set('last_user_id', json.data.id);",
                    "}"
                  ]
                }
              }
            ]
          }
        },
        {
          "name": "GET /api/v1/admin/users/:id",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/api/v1/admin/users/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "users",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{last_user_id}}",
                  "description": "Keycloak user UUID"
                }
              ]
            }
          }
        },
        {
          "name": "PUT /api/v1/admin/users/:id",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/api/v1/admin/users/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "users",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{last_user_id}}"
                }
              ]
            },
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"firstName\": \"Test\",\n  \"lastName\": \"User Updated\",\n  \"email\": \"test@example.com\",\n  \"enabled\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "DELETE /api/v1/admin/users/:id",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{base_url}}/api/v1/admin/users/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "users",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{last_user_id}}"
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Admin — API Clients",
      "item": [
        {
          "name": "GET /api/v1/admin/clients",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/api/v1/admin/clients",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "clients"
              ]
            },
            "description": "Lista klientów API (Keycloak service accounts). Wymaga ROLE_ADMIN."
          }
        },
        {
          "name": "POST /api/v1/admin/clients",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/api/v1/admin/clients",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "clients"
              ]
            },
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"clientId\": \"moj_klient_api\",\n  \"name\": \"Mój klient API\",\n  \"description\": \"Klient testowy\",\n  \"enabled\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "DELETE /api/v1/admin/clients/:clientId",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{base_url}}/api/v1/admin/clients/:clientId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "clients",
                ":clientId"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "moj_klient_api"
                }
              ]
            }
          }
        },
        {
          "name": "POST /api/v1/admin/clients/:clientId/regenerate-secret",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/api/v1/admin/clients/:clientId/regenerate-secret",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "clients",
                ":clientId",
                "regenerate-secret"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "moj_klient_api"
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Admin — Audit Logs",
      "item": [
        {
          "name": "GET /api/v1/admin/audit-logs",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/api/v1/admin/audit-logs?page=1&limit=25",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "audit-logs"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "limit",
                  "value": "25"
                },
                {
                  "key": "action",
                  "value": "",
                  "description": "Filtr akcji (np. CREATE, UPDATE, DELETE)",
                  "disabled": true
                },
                {
                  "key": "entity",
                  "value": "",
                  "description": "Filtr encji (np. User, Shipment)",
                  "disabled": true
                },
                {
                  "key": "userId",
                  "value": "",
                  "description": "Filtr po ID użytkownika",
                  "disabled": true
                }
              ]
            },
            "description": "Logi audytowe. Wymaga ROLE_ADMIN."
          }
        }
      ]
    },
    {
      "name": "Admin — Access Requests",
      "description": "Zarządzanie wnioskami o dostęp do API. Wymaga roli api_admin (token z rolą api_admin w Keycloak).",
      "item": [
        {
          "name": "Lista wniosków",
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/admin/access-requests?status=pending&page=1&limit=20",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "access-requests"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "pending",
                  "description": "pending | approved | rejected (opcjonalnie)"
                },
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "limit",
                  "value": "20"
                }
              ]
            },
            "description": "Zwraca paginowaną listę wniosków o dostęp. Filtry: status, page, limit."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "pm.test('Ma pole data', () => pm.expect(pm.response.json().data).to.be.an('array'));"
                ]
              }
            }
          ]
        },
        {
          "name": "Szczegół wniosku",
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/admin/access-requests/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "access-requests",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "UUID wniosku"
                }
              ]
            },
            "description": "Zwraca pełne dane jednego wniosku."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "pm.test('Ma id', () => pm.expect(pm.response.json().data.id).to.be.a('string'));"
                ]
              }
            }
          ]
        },
        {
          "name": "Zatwierdź wniosek",
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/admin/access-requests/:id/approve",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "access-requests",
                ":id",
                "approve"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "UUID wniosku"
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"scopes\": [\n    \"shipments:read\",\n    \"shipments:write\",\n    \"products:read\"\n  ],\n  \"notes\": \"Zatwierdzony po weryfikacji danych firmy.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Zatwierdza wniosek. Tworzy klienta w Keycloak i DB, zwraca client_id + client_secret (jednorazowo!). Wysyła email z credentials."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "const json = pm.response.json();",
                  "pm.test('Ma credentials', () => {",
                  "    pm.expect(json.data.credentials.client_id).to.be.a('string');",
                  "    pm.expect(json.data.credentials.client_secret).to.be.a('string');",
                  "});",
                  "if (json.data?.credentials?.client_id) {",
                  "    pm.collectionVariables.set('approved_client_id', json.data.credentials.client_id);",
                  "    console.log('client_id:', json.data.credentials.client_id);",
                  "    console.log('client_secret:', json.data.credentials.client_secret);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Odrzuć wniosek",
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/admin/access-requests/:id/reject",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "admin",
                "access-requests",
                ":id",
                "reject"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "UUID wniosku"
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reason\": \"Niekompletne dane firmy \\u2014 brak numeru NIP w rejestrze GUS.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Odrzuca wniosek. Wysyła email z powodem odrzucenia."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "pm.test('Status rejected', () => pm.expect(pm.response.json().data.status).to.equal('rejected'));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Pricing",
      "item": [
        {
          "name": "Calculate Price",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/api/v1/prices/calculate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "prices",
                "calculate"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"product_code\": \"LIST_POLECONY\",\n  \"weight_grams\": 100,\n  \"zone\": \"domestic\",\n  \"services\": [\n    \"tracking\",\n    \"confirmation\"\n  ],\n  \"declared_value_cents\": 0,\n  \"cod_amount_cents\": 0,\n  \"currency\": \"PLN\"\n}"
            },
            "description": "Calculate shipment price without creating a shipment. Returns base price + services breakdown."
          }
        },
        {
          "name": "List Price Lists",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/api/v1/prices/lists?active_only=true&page=1&limit=25",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "prices",
                "lists"
              ],
              "query": [
                {
                  "key": "product_code",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "date",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "active_only",
                  "value": "true"
                },
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "limit",
                  "value": "25"
                }
              ]
            },
            "description": "List available price lists with optional filtering by product code and date."
          }
        },
        {
          "name": "Get Price List Detail",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/api/v1/prices/lists/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "prices",
                "lists",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{price_list_id}}",
                  "description": "Price list UUID"
                }
              ]
            },
            "description": "Get detailed price list including all weight tier items."
          }
        },
        {
          "name": "Create Price List (Admin)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/api/v1/prices/lists",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "prices",
                "lists"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Cennik 2026 \\u2014 Test\",\n  \"product_code\": \"LIST_POLECONY\",\n  \"pricing_type\": \"local\",\n  \"valid_from\": \"2026-01-01\",\n  \"valid_to\": \"2026-12-31\",\n  \"is_active\": true,\n  \"items\": [\n    {\n      \"weight_from_grams\": 0,\n      \"weight_to_grams\": 500,\n      \"zone\": \"domestic\",\n      \"base_price_cents\": 560,\n      \"insurance_rate_permille\": 0,\n      \"cod_fee_cents\": 0,\n      \"additional_services\": {\n        \"tracking\": 100,\n        \"confirmation\": 250\n      }\n    }\n  ]\n}"
            },
            "description": "Create a new price list with weight tier items. Requires ROLE_ADMIN."
          }
        },
        {
          "name": "Update Price List (Admin)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{access_token}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/api/v1/prices/lists/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "prices",
                "lists",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{price_list_id}}",
                  "description": "Price list UUID"
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Cennik 2026 \\u2014 Test (updated)\",\n  \"product_code\": \"LIST_POLECONY\",\n  \"pricing_type\": \"local\",\n  \"valid_from\": \"2026-01-01\",\n  \"valid_to\": \"2026-12-31\",\n  \"is_active\": true,\n  \"items\": []\n}"
            },
            "description": "Update price list metadata. Requires ROLE_ADMIN."
          }
        }
      ]
    },
    {
      "name": "_Auth",
      "description": "Helpery do zarządzania tokenem",
      "item": [
        {
          "name": "Pobierz token (manual)",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "url": {
              "raw": "{{keycloak_url}}",
              "host": [
                "{{keycloak_url}}"
              ]
            },
            "header": [
              {
                "key": "Content-Type",
                "value": "application/x-www-form-urlencoded"
              }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                {
                  "key": "grant_type",
                  "value": "client_credentials"
                },
                {
                  "key": "client_id",
                  "value": "{{client_id}}"
                },
                {
                  "key": "client_secret",
                  "value": "{{client_secret}}"
                }
              ]
            },
            "description": "Ręczne pobranie tokenu. Normalnie token jest pobierany automatycznie przez Pre-request Script kolekcji."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Token obtained', () => {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json.access_token).to.be.a('string');",
                  "    pm.collectionVariables.set('access_token', json.access_token);",
                  "    pm.collectionVariables.set('token_expires_at', String(Math.floor(Date.now() / 1000) + json.expires_in));",
                  "    console.log('Token zapisany, wygasa za', json.expires_in, 's');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Wyczyść token",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/api/v1/health",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "health"
              ]
            },
            "description": "Uruchom ten request żeby wyczyścić token (wymusi pobranie nowego przy kolejnym requeście)."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set('access_token', '');",
                  "pm.collectionVariables.set('token_expires_at', '0');",
                  "console.log('Token wyczyszczony');"
                ]
              }
            }
          ]
        },
        {
          "name": "Pobierz token (proxy API)",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/auth/token",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "auth",
                "token"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"client_id\": \"{{client_id}}\",\n  \"client_secret\": \"{{client_secret}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Proxy do Keycloak — zwraca access_token. Alternatywa: bezpośredni request do Keycloak URL."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Token obtained via proxy', () => {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json.access_token).to.be.a('string');",
                  "    pm.collectionVariables.set('access_token', json.access_token);",
                  "    const expiresIn = json.expires_in || 600;",
                  "    pm.collectionVariables.set('token_expires_at', String(Math.floor(Date.now()/1000) + expiresIn));",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}