{
  "info": {
    "name": "Zazpay — Devolución de ventas (sandbox)",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "description": "Full test matrix for POST /commerce/return-transaction against the sandbox.\n\nSetup: fill `baseUrl`, `authUrl`, `clientId`, `clientSecret`, `storeId`, `salesmanIdentifier`, `clientUsername`. A Keycloak client-credentials token is fetched automatically before each request (cached until expiry).\n\nRun folders top-to-bottom with the Collection Runner — requests inside a folder chain state via collection variables.\n\nSandbox outcomes — two mechanisms: (1) MAIN: the sandbox-only `/commerce/resolve-transaction` simulates the client decision (`APPROVED`/`REJECTED`/`EXPIRED`/`CANCELED`/`RETURNED`); (2) alternative: sales auto-resolve ~5 seconds after creation — to APPROVED, or REJECTED when `folioExternal` contains `REJECTED`. Every create-sale in this collection seeds `NO_CANCEL` into `folioExternal` to disable that auto-transition, so the resolve requests never race the ~5s timer and runs are deterministic at any pacing.\n\nSandbox flags (substrings of `folioExternal`, combinable): `NO_CANCEL` → stays IN_PROGRESS · `REJECTED` → auto-REJECTED after ~5s · `COMMERCE_SETTLED` → DISCOUNT_NEXT_SETTLEMENT · `WITH_PAYMENTS` → mock event carries clientRefundPending · `NO_RETURN` → 409 RETURN-DECLINED."
  },
  "auth": {
    "type": "bearer",
    "bearer": [{ "key": "token", "value": "{{accessToken}}", "type": "string" }]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "const expiry = Number(pm.collectionVariables.get('tokenExpiry') || 0);",
          "if (!pm.collectionVariables.get('accessToken') || Date.now() > expiry) {",
          "  pm.sendRequest({",
          "    url: pm.collectionVariables.get('authUrl') + '/realms/commerces/protocol/openid-connect/token',",
          "    method: 'POST',",
          "    header: { 'Content-Type': 'application/x-www-form-urlencoded' },",
          "    body: { mode: 'urlencoded', urlencoded: [",
          "      { key: 'grant_type', value: 'client_credentials' },",
          "      { key: 'client_id', value: pm.collectionVariables.get('clientId') },",
          "      { key: 'client_secret', value: pm.collectionVariables.get('clientSecret') }",
          "    ]}",
          "  }, (err, res) => {",
          "    if (!err && res.code === 200) {",
          "      const j = res.json();",
          "      pm.collectionVariables.set('accessToken', j.access_token);",
          "      pm.collectionVariables.set('tokenExpiry', String(Date.now() + (j.expires_in - 30) * 1000));",
          "    } else {",
          "      console.error('Token fetch failed', err || res.code);",
          "    }",
          "  });",
          "}"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.sandbox.zazpay.mx",
      "description": "commerce-ms base URL (local: http://localhost:3000)"
    },
    {
      "key": "authUrl",
      "value": "https://auth.server.zazpay.mx",
      "description": "Keycloak base URL for your environment"
    },
    {
      "key": "clientId",
      "value": "",
      "description": "Keycloak client id (provided by Zazpay)"
    },
    {
      "key": "clientSecret",
      "value": "",
      "description": "Keycloak client secret"
    },
    {
      "key": "storeId",
      "value": "",
      "description": "Store id (external or internal)"
    },
    {
      "key": "salesmanIdentifier",
      "value": "",
      "description": "Salesman reference"
    },
    {
      "key": "clientUsername",
      "value": "",
      "description": "End-client username/identifier"
    },
    {
      "key": "saleTotal",
      "value": "1500",
      "description": "Amount used for every test sale"
    },
    {
      "key": "webhookTargetUrl",
      "value": "https://webhook.site/REPLACE-ME",
      "description": "Target for the TRANSACTION_RETURNED webhook test"
    }
  ],
  "item": [
    {
      "name": "1 · Return feliz → NOT_YET_SETTLED + replay idempotente",
      "item": [
        {
          "name": "Crear venta",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set('f1_folioExternal', 'PM-NO_CANCEL-RET-' + Date.now());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201/200', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.test('status IN_PROGRESS', () => pm.expect(data.status).to.eql('IN_PROGRESS'));",
                  "pm.collectionVariables.set('f1_folio', data.folio);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/generate-sale",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "generate-sale"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"total\": {{saleTotal}},\n  \"clientUsername\": \"{{clientUsername}}\",\n  \"salesmanIdentifier\": \"{{salesmanIdentifier}}\",\n  \"storeId\": \"{{storeId}}\",\n  \"folioExternal\": \"{{f1_folioExternal}}\"\n}"
            }
          }
        },
        {
          "name": "Resolver → APPROVED (sandbox)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200/201', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.test('APPROVED', () => pm.expect(data.status).to.eql('APPROVED'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/resolve-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "resolve-transaction"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folio\": {{f1_folio}},\n  \"status\": \"APPROVED\"\n}"
            }
          }
        },
        {
          "name": "Return (sin amount)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.expect(pm.response.code).to.eql(200));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.test('status RETURNED', () => pm.expect(data.status).to.eql('RETURNED'));",
                  "pm.test('returnedAmount = total', () => pm.expect(Number(data.returnedAmount)).to.eql(Number(pm.collectionVariables.get('saleTotal'))));",
                  "pm.test('settlementImpact NOT_YET_SETTLED', () => pm.expect(data.settlementImpact).to.eql('NOT_YET_SETTLED'));",
                  "pm.test('returnedAt presente', () => pm.expect(data.returnedAt).to.be.a('string'));",
                  "pm.collectionVariables.set('f1_firstReturn', JSON.stringify(data));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/return-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "return-transaction"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folio\": {{f1_folio}},\n  \"returnReason\": \"Producto defectuoso (prueba Postman)\"\n}"
            }
          }
        },
        {
          "name": "Return replay → body idéntico, 200",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK en replay (nunca error)', () => pm.expect(pm.response.code).to.eql(200));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "const first = JSON.parse(pm.collectionVariables.get('f1_firstReturn'));",
                  "pm.test('replay idéntico al primer response', () => pm.expect(data).to.eql(first));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/return-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "return-transaction"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folio\": {{f1_folio}},\n  \"returnReason\": \"Producto defectuoso (prueba Postman)\"\n}"
            }
          }
        },
        {
          "name": "transaction-status = RETURNED",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200/201', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.test('RETURNED', () => pm.expect(data.status).to.eql('RETURNED'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/transaction-status",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "transaction-status"]
            },
            "body": { "mode": "raw", "raw": "{\n  \"folio\": {{f1_folio}}\n}" }
          }
        }
      ]
    },
    {
      "name": "2 · Comercio ya pagado → DISCOUNT_NEXT_SETTLEMENT",
      "item": [
        {
          "name": "Crear venta (flag COMMERCE_SETTLED)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set('f2_folioExternal', 'PM-NO_CANCEL-COMMERCE_SETTLED-' + Date.now());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201/200', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.collectionVariables.set('f2_folio', data.folio);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/generate-sale",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "generate-sale"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"total\": {{saleTotal}},\n  \"clientUsername\": \"{{clientUsername}}\",\n  \"salesmanIdentifier\": \"{{salesmanIdentifier}}\",\n  \"storeId\": \"{{storeId}}\",\n  \"folioExternal\": \"{{f2_folioExternal}}\"\n}"
            }
          }
        },
        {
          "name": "Resolver → APPROVED",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200/201', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/resolve-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "resolve-transaction"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folio\": {{f2_folio}},\n  \"status\": \"APPROVED\"\n}"
            }
          }
        },
        {
          "name": "Return → DISCOUNT_NEXT_SETTLEMENT",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.expect(pm.response.code).to.eql(200));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.test('RETURNED', () => pm.expect(data.status).to.eql('RETURNED'));",
                  "pm.test('settlementImpact DISCOUNT_NEXT_SETTLEMENT', () => pm.expect(data.settlementImpact).to.eql('DISCOUNT_NEXT_SETTLEMENT'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/return-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "return-transaction"]
            },
            "body": { "mode": "raw", "raw": "{\n  \"folio\": {{f2_folio}}\n}" }
          }
        }
      ]
    },
    {
      "name": "3 · Cliente con abonos (WITH_PAYMENTS) → return no se bloquea",
      "item": [
        {
          "name": "Crear venta (flag WITH_PAYMENTS)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set('f3_folioExternal', 'PM-NO_CANCEL-WITH_PAYMENTS-' + Date.now());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201/200', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.collectionVariables.set('f3_folio', data.folio);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/generate-sale",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "generate-sale"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"total\": {{saleTotal}},\n  \"clientUsername\": \"{{clientUsername}}\",\n  \"salesmanIdentifier\": \"{{salesmanIdentifier}}\",\n  \"storeId\": \"{{storeId}}\",\n  \"folioExternal\": \"{{f3_folioExternal}}\"\n}"
            }
          }
        },
        {
          "name": "Resolver → APPROVED",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200/201', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/resolve-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "resolve-transaction"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folio\": {{f3_folio}},\n  \"status\": \"APPROVED\"\n}"
            }
          }
        },
        {
          "name": "Return → 200 (refund interno nunca se expone)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.expect(pm.response.code).to.eql(200));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.test('RETURNED', () => pm.expect(data.status).to.eql('RETURNED'));",
                  "pm.test('NOT_YET_SETTLED', () => pm.expect(data.settlementImpact).to.eql('NOT_YET_SETTLED'));",
                  "pm.test('sin campos de refund del cliente', () => {",
                  "  pm.expect(data).to.not.have.property('clientRefund');",
                  "  pm.expect(data).to.not.have.property('clientRefundPending');",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/return-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "return-transaction"]
            },
            "body": { "mode": "raw", "raw": "{\n  \"folio\": {{f3_folio}}\n}" }
          }
        }
      ]
    },
    {
      "name": "4 · Rechazada (NO_RETURN) → 409 RETURN-DECLINED",
      "item": [
        {
          "name": "Crear venta (flag NO_RETURN)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set('f4_folioExternal', 'PM-NO_CANCEL-NO_RETURN-' + Date.now());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201/200', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.collectionVariables.set('f4_folio', data.folio);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/generate-sale",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "generate-sale"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"total\": {{saleTotal}},\n  \"clientUsername\": \"{{clientUsername}}\",\n  \"salesmanIdentifier\": \"{{salesmanIdentifier}}\",\n  \"storeId\": \"{{storeId}}\",\n  \"folioExternal\": \"{{f4_folioExternal}}\"\n}"
            }
          }
        },
        {
          "name": "Resolver → APPROVED",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200/201', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/resolve-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "resolve-transaction"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folio\": {{f4_folio}},\n  \"status\": \"APPROVED\"\n}"
            }
          }
        },
        {
          "name": "Return → 409 RETURN-DECLINED",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('409 Conflict', () => pm.expect(pm.response.code).to.eql(409));",
                  "pm.test('errorCode RETURN-DECLINED', () => pm.expect(pm.response.text()).to.include('RETURN-DECLINED'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/return-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "return-transaction"]
            },
            "body": { "mode": "raw", "raw": "{\n  \"folio\": {{f4_folio}}\n}" }
          }
        }
      ]
    },
    {
      "name": "5 · Errores",
      "item": [
        {
          "name": "Folio inexistente → 404 TRANSACTION-NOT-FOUND",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('404 Not Found', () => pm.expect(pm.response.code).to.eql(404));",
                  "pm.test('TRANSACTION-NOT-FOUND', () => pm.expect(pm.response.text()).to.include('TRANSACTION-NOT-FOUND'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/return-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "return-transaction"]
            },
            "body": { "mode": "raw", "raw": "{\n  \"folio\": 99999999\n}" }
          }
        },
        {
          "name": "Crear venta IN_PROGRESS",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set('f5a_folioExternal', 'PM-NO_CANCEL-ERR-' + Date.now());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201/200', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.collectionVariables.set('f5a_folio', data.folio);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/generate-sale",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "generate-sale"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"total\": {{saleTotal}},\n  \"clientUsername\": \"{{clientUsername}}\",\n  \"salesmanIdentifier\": \"{{salesmanIdentifier}}\",\n  \"storeId\": \"{{storeId}}\",\n  \"folioExternal\": \"{{f5a_folioExternal}}\"\n}"
            }
          }
        },
        {
          "name": "Return de IN_PROGRESS → 400 TRANSACTION-IN-PROGRESS-USE-CANCEL",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('400 Bad Request', () => pm.expect(pm.response.code).to.eql(400));",
                  "pm.test('TRANSACTION-IN-PROGRESS-USE-CANCEL', () => pm.expect(pm.response.text()).to.include('TRANSACTION-IN-PROGRESS-USE-CANCEL'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/return-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "return-transaction"]
            },
            "body": { "mode": "raw", "raw": "{\n  \"folio\": {{f5a_folio}}\n}" }
          }
        },
        {
          "name": "Cancelar la venta IN_PROGRESS",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200/201', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.test('CANCELED', () => pm.expect(data.status).to.eql('CANCELED'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/cancel-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "cancel-transaction"]
            },
            "body": { "mode": "raw", "raw": "{\n  \"folio\": {{f5a_folio}}\n}" }
          }
        },
        {
          "name": "Return de CANCELED → 400 TRANSACTION-NOT-RETURNABLE",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('400 Bad Request', () => pm.expect(pm.response.code).to.eql(400));",
                  "pm.test('TRANSACTION-NOT-RETURNABLE', () => pm.expect(pm.response.text()).to.include('TRANSACTION-NOT-RETURNABLE'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/return-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "return-transaction"]
            },
            "body": { "mode": "raw", "raw": "{\n  \"folio\": {{f5a_folio}}\n}" }
          }
        },
        {
          "name": "Crear venta + aprobar (para mismatch)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set('f5b_folioExternal', 'PM-NO_CANCEL-MISMATCH-' + Date.now());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201/200', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.collectionVariables.set('f5b_folio', data.folio);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/generate-sale",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "generate-sale"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"total\": {{saleTotal}},\n  \"clientUsername\": \"{{clientUsername}}\",\n  \"salesmanIdentifier\": \"{{salesmanIdentifier}}\",\n  \"storeId\": \"{{storeId}}\",\n  \"folioExternal\": \"{{f5b_folioExternal}}\"\n}"
            }
          }
        },
        {
          "name": "Resolver → APPROVED (mismatch)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200/201', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/resolve-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "resolve-transaction"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folio\": {{f5b_folio}},\n  \"status\": \"APPROVED\"\n}"
            }
          }
        },
        {
          "name": "Return con amount ≠ total → 400 RETURN-AMOUNT-MISMATCH",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set('f5b_badAmount', String(Number(pm.collectionVariables.get('saleTotal')) + 1));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('400 Bad Request', () => pm.expect(pm.response.code).to.eql(400));",
                  "pm.test('RETURN-AMOUNT-MISMATCH', () => pm.expect(pm.response.text()).to.include('RETURN-AMOUNT-MISMATCH'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/return-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "return-transaction"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folio\": {{f5b_folio}},\n  \"amount\": {{f5b_badAmount}}\n}"
            }
          }
        },
        {
          "name": "Return correcto (amount = total) → 200",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.expect(pm.response.code).to.eql(200));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.test('RETURNED', () => pm.expect(data.status).to.eql('RETURNED'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/return-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "return-transaction"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folio\": {{f5b_folio}},\n  \"amount\": {{saleTotal}}\n}"
            }
          }
        },
        {
          "name": "Replay con amount distinto → 400 aunque ya esté RETURNED",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('400 Bad Request', () => pm.expect(pm.response.code).to.eql(400));",
                  "pm.test('RETURN-AMOUNT-MISMATCH', () => pm.expect(pm.response.text()).to.include('RETURN-AMOUNT-MISMATCH'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/return-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "return-transaction"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folio\": {{f5b_folio}},\n  \"amount\": {{f5b_badAmount}}\n}"
            }
          }
        }
      ]
    },
    {
      "name": "6 · Forzar RETURNED vía resolve-transaction",
      "description": "Force-tool de sandbox: aplica RETURNED sobre una venta APPROVED y dispara los mismos webhooks que return-transaction. Intencionalmente ignora el flag NO_RETURN.",
      "item": [
        {
          "name": "Crear venta",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set('f6_folioExternal', 'PM-NO_CANCEL-FORCE-' + Date.now());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201/200', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.collectionVariables.set('f6_folio', data.folio);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/generate-sale",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "generate-sale"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"total\": {{saleTotal}},\n  \"clientUsername\": \"{{clientUsername}}\",\n  \"salesmanIdentifier\": \"{{salesmanIdentifier}}\",\n  \"storeId\": \"{{storeId}}\",\n  \"folioExternal\": \"{{f6_folioExternal}}\"\n}"
            }
          }
        },
        {
          "name": "Resolver → APPROVED",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200/201', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/resolve-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "resolve-transaction"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folio\": {{f6_folio}},\n  \"status\": \"APPROVED\"\n}"
            }
          }
        },
        {
          "name": "Resolver → RETURNED (force)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200/201', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.test('RETURNED', () => pm.expect(data.status).to.eql('RETURNED'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/resolve-transaction",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "resolve-transaction"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folio\": {{f6_folio}},\n  \"status\": \"RETURNED\"\n}"
            }
          }
        },
        {
          "name": "transaction-status = RETURNED",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200/201', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.test('RETURNED', () => pm.expect(data.status).to.eql('RETURNED'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/commerce/transaction-status",
              "host": ["{{baseUrl}}"],
              "path": ["commerce", "transaction-status"]
            },
            "body": { "mode": "raw", "raw": "{\n  \"folio\": {{f6_folio}}\n}" }
          }
        }
      ]
    },
    {
      "name": "7 · Webhook TRANSACTION_RETURNED (requiere rol ADMIN_ZAZPAY_HUB)",
      "description": "El CRUD de webhooks exige el rol ADMIN_ZAZPAY_HUB — normalmente se configura desde el Zazpay Hub. Si tu client credentials no tiene ese rol, estas requests devuelven 403. OJO: en el template del payload, Postman deja los placeholders {{...}} sin resolver y los envía literales — eso es lo que el servicio espera; no definas variables de Postman con esos nombres.",
      "item": [
        {
          "name": "Opciones de eventos (incluye TRANSACTION_RETURNED)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.expect(pm.response.code).to.eql(200));",
                  "pm.test('lista TRANSACTION_RETURNED con returnedAmount', () => {",
                  "  const text = pm.response.text();",
                  "  pm.expect(text).to.include('TRANSACTION_RETURNED');",
                  "  pm.expect(text).to.include('returnedAmount');",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/webhooks/options",
              "host": ["{{baseUrl}}"],
              "path": ["webhooks", "options"]
            }
          }
        },
        {
          "name": "Crear webhook TRANSACTION_RETURNED",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201/200', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "const data = pm.response.json().data || pm.response.json();",
                  "pm.collectionVariables.set('f7_webhookId', data.id);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/webhooks",
              "host": ["{{baseUrl}}"],
              "path": ["webhooks"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"verb\": \"POST\",\n  \"url\": \"{{webhookTargetUrl}}\",\n  \"event\": \"TRANSACTION_RETURNED\",\n  \"contentType\": \"APPLICATION_JSON\",\n  \"payload\": \"{\\\"folio\\\": \\\"{{folio}}\\\", \\\"status\\\": \\\"{{status}}\\\", \\\"returnedAmount\\\": \\\"{{returnedAmount}}\\\", \\\"settlementImpact\\\": \\\"{{settlementImpact}}\\\", \\\"returnedAt\\\": \\\"{{returnedAt}}\\\", \\\"returnReason\\\": \\\"{{returnReason}}\\\"}\"\n}"
            }
          }
        },
        {
          "name": "Probar webhook con payload mock",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.expect(pm.response.code).to.eql(200));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/webhooks/test?webhookId={{f7_webhookId}}",
              "host": ["{{baseUrl}}"],
              "path": ["webhooks", "test"],
              "query": [{ "key": "webhookId", "value": "{{f7_webhookId}}" }]
            }
          }
        },
        {
          "name": "Logs de entregas",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.expect(pm.response.code).to.eql(200));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/webhooks/logs",
              "host": ["{{baseUrl}}"],
              "path": ["webhooks", "logs"]
            }
          }
        }
      ]
    }
  ]
}
