{
  "info": {
    "_postman_id": "d8f4e2a1-6b3d-4c5e-8d7f-4e7a9c8d1e3f",
    "name": "Kesles PSP Integration API",
    "description": "Collection untuk test integrasi /api/psp/v1/* — endpoint yang di-call oleh payment.kesles.com (Tier-1) atau PSP bank (Tier-2).\n\nAuth: HMAC-SHA256 (lihat api-internal/psp-integration-contract.md §2).\n\nPre-request script otomatis generate signature + timestamp + request ID untuk setiap request. Tinggal set `psp_key_id` + `psp_hmac_secret` di environment.\n\nEndpoint groups:\n- Lookup (GET, semua tier): /merchants list/search/by-*, /devices, /by-serial\n- Tier-1-only write: PATCH /nmid-assignment (Scheme A). Event push: POST /psp/v1/events + /psp/v1/settlements (Scheme B)",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// HMAC signing pre-request — pilih skema per endpoint:",
          "//   Scheme A (Lookup, /api/psp/v1/*): X-API-*, unix, X-Signature: hmac-sha256=<hex>, {ts}\\n{METHOD}\\n{path+query}\\n{body}",
          "//   Scheme B (Events, /psp/v1/*):     X-PSP-*, RFC3339, X-PSP-Signature: <hex polos>, {method}\\n{path}\\n{ts}\\n{body}",
          "const keyID = pm.environment.get('psp_key_id') || pm.collectionVariables.get('psp_key_id');",
          "const secret = pm.environment.get('psp_hmac_secret') || pm.collectionVariables.get('psp_hmac_secret');",
          "if (!keyID || !secret) { console.warn('psp_key_id / psp_hmac_secret belum di-set — request akan 401'); return; }",
          "",
          "const method = (pm.request.method || 'GET').toUpperCase();",
          "const url = pm.request.url;",
          "const path0 = url.getPath();",
          "const query = url.getQueryString();",
          "let bodyStr = '';",
          "if (pm.request.body && pm.request.body.mode === 'raw') { bodyStr = pm.request.body.raw || ''; }",
          "const isEvents = path0.indexOf('/psp/v1/') === 0;  // Events API => Scheme B",
          "",
          "if (isEvents) {",
          "  const ts = new Date().toISOString().replace(/\\.\\d{3}Z$/, 'Z');  // RFC3339 tanpa milidetik",
          "  const stringToSign = method + '\\n' + path0 + '\\n' + ts + '\\n' + bodyStr;",
          "  const sig = CryptoJS.HmacSHA256(stringToSign, secret).toString(CryptoJS.enc.Hex);",
          "  let eid = '';",
          "  try { eid = JSON.parse(bodyStr).external_event_id || ''; } catch (e) {}",
          "  pm.request.headers.upsert({ key: 'X-PSP-Key-ID', value: keyID });",
          "  pm.request.headers.upsert({ key: 'X-PSP-Timestamp', value: ts });",
          "  pm.request.headers.upsert({ key: 'X-PSP-Signature', value: sig });",
          "  pm.request.headers.upsert({ key: 'X-Request-ID', value: pm.variables.replaceIn('{{$guid}}') });",
          "  pm.request.headers.upsert({ key: 'X-Idempotency-Key', value: eid });",
          "  console.log('[Scheme B] string-to-sign:\\n' + stringToSign);",
          "} else {",
          "  const ts = Math.floor(Date.now() / 1000);  // unix seconds",
          "  let path = path0; if (query) { path = path + '?' + query; }",
          "  const stringToSign = ts + '\\n' + method + '\\n' + path + '\\n' + bodyStr;",
          "  const sig = CryptoJS.HmacSHA256(stringToSign, secret).toString(CryptoJS.enc.Hex);",
          "  pm.request.headers.upsert({ key: 'X-API-Key-ID', value: keyID });",
          "  pm.request.headers.upsert({ key: 'X-Timestamp', value: String(ts) });",
          "  pm.request.headers.upsert({ key: 'X-Signature', value: 'hmac-sha256=' + sig });",
          "  pm.request.headers.upsert({ key: 'X-Request-ID', value: pm.variables.replaceIn('{{$guid}}') });",
          "  console.log('[Scheme A] string-to-sign:\\n' + stringToSign);",
          "}"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "pm.test('Response time < 2000ms', () => pm.expect(pm.response.responseTime).to.be.below(2000));",
          "if (pm.response.code === 200 || pm.response.code === 201) {",
          "  pm.test('Response is valid JSON', () => { pm.expect(() => pm.response.json()).not.to.throw(); });",
          "}",
          "if (pm.response.code === 401) {",
          "  console.warn('401 Unauthorized — check psp_key_id + psp_hmac_secret in environment, or timestamp drift');",
          "}"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "base_url",
      "value": "http://localhost:8080",
      "type": "string"
    },
    {
      "key": "psp_key_id",
      "value": "psp-dev-key-001",
      "type": "string"
    },
    {
      "key": "psp_hmac_secret",
      "value": "",
      "type": "secret"
    },
    {
      "key": "merchant_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "merchant_code",
      "value": "",
      "type": "string"
    },
    {
      "key": "nmid",
      "value": "",
      "type": "string"
    },
    {
      "key": "mid",
      "value": "",
      "type": "string"
    },
    {
      "key": "serial_number",
      "value": "",
      "type": "string"
    },
    {
      "key": "outlet_id",
      "value": "",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "1. Merchant Lookup (PSP → kesles_merchant)",
      "description": "Endpoint yang di-call payment.kesles.com untuk lookup merchant data. Hanya return merchant active + NMID assigned.",
      "item": [
        {
          "name": "GET /api/psp/v1/merchants (list active)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants?limit=50",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50"
                },
                {
                  "key": "cursor",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "updated_since",
                  "value": "2026-05-01T00:00:00Z",
                  "disabled": true,
                  "description": "Delta sync"
                },
                {
                  "key": "bank_code",
                  "value": "BMRI",
                  "disabled": true
                },
                {
                  "key": "category_code",
                  "value": "5411",
                  "disabled": true
                },
                {
                  "key": "include_outlets",
                  "value": "false",
                  "disabled": true
                }
              ]
            },
            "description": "List merchants aktif dengan NMID assigned. Cursor paginated. Rate limit: 60 req/menit."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const res = pm.response.json();",
                  "  pm.test('Response has items', () => pm.expect(res).to.have.property('items'));",
                  "  pm.test('Response has pagination', () => pm.expect(res).to.have.property('pagination'));",
                  "  pm.test('Meta.active_only=true', () => pm.expect(res.meta.active_only).to.be.true);",
                  "  if (res.items.length > 0) {",
                  "    const m = res.items[0];",
                  "    pm.collectionVariables.set('merchant_id', m.id || '');",
                  "    pm.collectionVariables.set('merchant_code', m.merchant_code || '');",
                  "    pm.collectionVariables.set('nmid', m.nmid || '');",
                  "    pm.collectionVariables.set('mid', m.mid || '');",
                  "  }",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /api/psp/v1/merchants/{id}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants/{{merchant_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants",
                "{{merchant_id}}"
              ]
            }
          }
        },
        {
          "name": "GET /api/psp/v1/merchants/{id}?include_outlets=true&include_owners=true",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants/{{merchant_id}}?include_outlets=true&include_owners=true",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants",
                "{{merchant_id}}"
              ],
              "query": [
                {
                  "key": "include_outlets",
                  "value": "true"
                },
                {
                  "key": "include_owners",
                  "value": "true"
                }
              ]
            }
          }
        },
        {
          "name": "GET /api/psp/v1/merchants/by-nmid/{nmid}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants/by-nmid/{{nmid}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants",
                "by-nmid",
                "{{nmid}}"
              ]
            },
            "description": "Reverse lookup by NMID. Hot endpoint — PSP call tiap transaksi. Rate limit: 1000 req/menit."
          }
        },
        {
          "name": "GET /api/psp/v1/merchants/by-code/{merchant_code}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants/by-code/{{merchant_code}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants",
                "by-code",
                "{{merchant_code}}"
              ]
            },
            "description": "Lookup merchant by Kesles canonical merchant_code (e.g. MRC-xxxxxxxxxxxxxxxx). Case-insensitive. Returns same MerchantDetailResponse shape as /merchants/{id}."
          }
        },
        {
          "name": "GET /api/psp/v1/merchants/by-mid/{mid}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants/by-mid/{{mid}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants",
                "by-mid",
                "{{mid}}"
              ]
            },
            "description": "Lookup merchant by acquirer MID (15-digit QRIS Merchant ID). Returns same MerchantDetailResponse shape as /merchants/{id}."
          }
        },
        {
          "name": "GET /api/psp/v1/merchants/by-serial/{serial_number}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants/by-serial/{{serial_number}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants",
                "by-serial",
                "{{serial_number}}"
              ]
            },
            "description": "Reverse lookup serial_number → merchant + device info (§3.6.6). Use when PSP receives error/settlement keyed only by SN. Tier-3 caller receives device.serial_number empty (PII-gated). Rate limit: 600 req/menit."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const res = pm.response.json();",
                  "  pm.test('Response has merchant', () => pm.expect(res).to.have.property('merchant'));",
                  "  pm.test('Response has device', () => pm.expect(res).to.have.property('device'));",
                  "  pm.test('device.serial_number matches request', () => pm.expect(res.device.serial_number || '').to.satisfy(s => s === '' || s.toLowerCase() === pm.collectionVariables.get('serial_number').toLowerCase()));",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "GET /api/psp/v1/merchants/search (by phone)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants/search?phone=081234567890",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants",
                "search"
              ],
              "query": [
                {
                  "key": "phone",
                  "value": "081234567890"
                },
                {
                  "key": "email",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "npwp",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "nmid",
                  "value": "",
                  "disabled": true
                }
              ]
            }
          }
        },
        {
          "name": "GET /api/psp/v1/merchants/{id}/outlets",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants/{{merchant_id}}/outlets",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants",
                "{{merchant_id}}",
                "outlets"
              ]
            }
          }
        },
        {
          "name": "GET /api/psp/v1/merchants/{id}/owners",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants/{{merchant_id}}/owners",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants",
                "{{merchant_id}}",
                "owners"
              ]
            },
            "description": "List owner/manager — untuk notifikasi kritis (refund, dispute). Phone+email di-mask."
          }
        },
        {
          "name": "GET /api/psp/v1/merchants/{id}/devices",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants/{{merchant_id}}/devices",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants",
                "{{merchant_id}}",
                "devices"
              ]
            },
            "description": "List semua device terpasang di merchant + summary aktif/inaktif (§3.6.5). Backed by view psp.v_active_merchant_devices (mig 126). Tier-3 caller receives items[].serial_number empty (PII-gated). Rate limit: 300 req/menit."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const res = pm.response.json();",
                  "  pm.test('Response has items', () => pm.expect(res).to.have.property('items'));",
                  "  pm.test('Response has summary', () => pm.expect(res).to.have.property('summary'));",
                  "  pm.test('Summary.total = items.length', () => pm.expect(res.summary.total).to.equal(res.items.length));",
                  "  pm.test('active + inactive = total', () => pm.expect(res.summary.active + res.summary.inactive).to.equal(res.summary.total));",
                  "  if (res.items.length > 0 && res.items[0].serial_number) {",
                  "    pm.collectionVariables.set('serial_number', res.items[0].serial_number);",
                  "  }",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "PATCH /api/psp/v1/merchants/{id}/nmid-assignment",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"bank_code\": \"BMRI\",\n  \"nmid\": \"ID102326873XXXX\",\n  \"qris_static_payload\": \"00020101021226580013ID102326873XXXX...\",\n  \"qris_static_url_image\": \"https://cdn.kesles.id/qr/merchant-uuid.png\",\n  \"psp_registration_id\": \"psp-reg-uuid-123\",\n  \"assigned_at\": \"2026-05-23T10:00:00Z\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants/{{merchant_id}}/nmid-assignment",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants",
                "{{merchant_id}}",
                "nmid-assignment"
              ]
            },
            "description": "PSP callback setelah bank assign NMID. kesles_merchant update merchant record + trigger mobile push ke owner."
          }
        }
      ]
    },
    {
      "name": "2. Event Receiver (PSP → kesles_merchant)",
      "description": "payment.kesles.com forward event dari bank (transaction, settlement, refund, nmid status) ke kesles_merchant.",
      "item": [
        {
          "name": "POST /psp/v1/events — transaction (success)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_event_id\": \"psp-evt-{{$guid}}\",\n  \"event_type\": \"transaction.success\",\n  \"transaction_code\": \"psp-txn-{{$guid}}\",\n  \"external_reference\": \"BI20260523142345XXXX\",\n  \"nmid\": \"{{nmid}}\",\n  \"gross_amount\": 75000,\n  \"mdr_rate\": 0.007,\n  \"mdr_fee_amount\": 525,\n  \"mdr_fee_kesles\": 225,\n  \"mdr_fee_bank\": 300,\n  \"currency\": \"IDR\",\n  \"transaction_at\": \"2026-05-23T14:23:45Z\",\n  \"payer_bank_code\": \"014\",\n  \"payer_bank_name\": \"BCA\",\n  \"payer_name_masked\": \"Budi S.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/psp/v1/events",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "psp",
                "v1",
                "events"
              ]
            },
            "description": "Push satu event transaksi sukses (Flow A/B ditentukan NMID). Scheme B. Body FLAT + external_event_id."
          }
        },
        {
          "name": "POST /psp/v1/settlements — settlement",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_event_id\": \"psp-evt-sett-{{$guid}}\",\n  \"psp_settlement_id\": \"psp-sett-{{$guid}}\",\n  \"nmid\": \"{{nmid}}\",\n  \"batch_number\": \"SETT-20260524-001\",\n  \"bank_code\": \"BMRI\",\n  \"transaction_count\": 48,\n  \"gross_amount\": 3240000,\n  \"mdr_total\": 22680,\n  \"net_amount\": 3217320,\n  \"expected_date\": \"2026-05-24\",\n  \"settled_at\": \"2026-05-24T08:00:00Z\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/psp/v1/settlements",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "psp",
                "v1",
                "settlements"
              ]
            },
            "description": "Push event settlement. Field wajib: external_event_id + psp_settlement_id + nmid."
          }
        },
        {
          "name": "POST /psp/v1/events — refund (transaction.refunded)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_event_id\": \"psp-evt-refund-{{$guid}}\",\n  \"event_type\": \"transaction.refunded\",\n  \"transaction_code\": \"psp-txn-original-uuid\",\n  \"external_reference\": \"psp-refund-{{$guid}}\",\n  \"nmid\": \"{{nmid}}\",\n  \"gross_amount\": 75000,\n  \"currency\": \"IDR\",\n  \"transaction_at\": \"2026-05-23T15:00:00Z\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/psp/v1/events",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "psp",
                "v1",
                "events"
              ]
            },
            "description": "Refund dikirim sebagai event_type transaction.refunded via /psp/v1/events (bukan endpoint terpisah)."
          }
        },
        {
          "name": "POST /psp/v1/events — nmid.status_changed",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_event_id\": \"psp-evt-nmid-status-{{$guid}}\",\n  \"event_type\": \"nmid.status_changed\",\n  \"nmid\": \"{{nmid}}\",\n  \"nmid_old_status\": \"active\",\n  \"nmid_new_status\": \"suspended\",\n  \"nmid_reason\": \"fraud_detected\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/psp/v1/events",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "psp",
                "v1",
                "events"
              ]
            },
            "description": "Perubahan status NMID (active/suspended/terminated) via /psp/v1/events. Body FLAT."
          }
        }
      ]
    },
    {
      "name": "3. Idempotency & Duplicate Detection",
      "description": "Test idempotency — kirim external_event_id yang sama 2×; response kedua HTTP 200 {\"status\":\"duplicate_skipped\"}.",
      "item": [
        {
          "name": "STEP 1: Kirim event dgn external_event_id tetap",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_event_id\": \"idempotency-test-fixed-event-001\",\n  \"event_type\": \"transaction.success\",\n  \"transaction_code\": \"idempotency-test-txn-001\",\n  \"nmid\": \"{{nmid}}\",\n  \"gross_amount\": 100000,\n  \"currency\": \"IDR\",\n  \"external_reference\": \"BI-IDEMPOTENCY-001\",\n  \"transaction_at\": \"2026-05-23T14:23:45Z\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/psp/v1/events",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "psp",
                "v1",
                "events"
              ]
            },
            "description": "Kirim pertama — expect HTTP 200 status=success (bukan duplicate_skipped)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const res = pm.response.json();",
                  "  pm.test('First send bukan duplikat', () => pm.expect(res.status).to.not.equal('duplicate_skipped'));",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "STEP 2: Kirim ulang event sama (expect status: duplicate_skipped)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_event_id\": \"idempotency-test-fixed-event-001\",\n  \"event_type\": \"transaction.success\",\n  \"transaction_code\": \"idempotency-test-txn-001\",\n  \"nmid\": \"{{nmid}}\",\n  \"gross_amount\": 100000,\n  \"currency\": \"IDR\",\n  \"external_reference\": \"BI-IDEMPOTENCY-001\",\n  \"transaction_at\": \"2026-05-23T14:23:45Z\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/psp/v1/events",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "psp",
                "v1",
                "events"
              ]
            },
            "description": "Kirim kedua dengan external_event_id sama — expect HTTP 200 {\"status\":\"duplicate_skipped\"}."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const res = pm.response.json();",
                  "  pm.test('Second send = duplicate_skipped', () => pm.expect(res.status).to.equal('duplicate_skipped'));",
                  "}"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "4. Auth Failure Scenarios",
      "description": "Test HMAC verification — skenario yang harus 401.",
      "item": [
        {
          "name": "[401] Missing signature headers",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants"
              ]
            },
            "description": "Skip pre-request script via manual removal untuk skenario ini. Expect 401 missing_auth_headers."
          }
        },
        {
          "name": "[401] Wrong secret (signature mismatch)",
          "description": "Set env `psp_hmac_secret` ke nilai salah sebelum run. Expect 401 invalid_signature.",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants"
              ]
            }
          }
        },
        {
          "name": "[401] Expired timestamp",
          "description": "Edit pre-request script sementara: ganti timestamp jadi `Date.now() - 600000` (10 menit lalu). Expect 401 timestamp_expired.",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants"
              ]
            }
          }
        },
        {
          "name": "[404] Merchant not active",
          "description": "GET merchant yang status=pending/suspended/deleted. Expect 404 merchant_not_found (hidden dari PSP).",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/psp/v1/merchants/00000000-0000-0000-0000-000000000000",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "psp",
                "v1",
                "merchants",
                "00000000-0000-0000-0000-000000000000"
              ]
            }
          }
        }
      ]
    }
  ]
}
