{
  "info": {
    "name": "Booking Window Hotel Markup (admin) v2",
    "description": "Hotel markup groups (fdk_hotels.hotel_markup_group), their markup conditions (fdk_hotels.hotel_markup) and agent assignment (fdk_holidays.website_users.hotel_markup_group_id).\n\nAuth: Bearer token of a staff / admin user (staffs table). Agent tokens get 403.\nSet base_url, staff_token and website_user_id, then run top to bottom; create requests save the ids later requests use.\n\nPricing rule: agent's group (or the default group) -> matching active markup rows -> highest score -> price + markup% - discount% of the supplier price. Changes reach every server process within 60 seconds (in-memory cache).\n\nCREATE: save_hotel_markup_group is the only create API - one call saves the group, all its markups and (optionally) the agents, in one transaction. Send id to update with the same API. The other requests list / read / edit / delete.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "_postman_id": "d7f17416-84a0-4948-b5c7-e59aaa8c85f3"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{staff_token}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://<booking-window-hotel-api-host>"
    },
    {
      "key": "staff_token",
      "value": "<staff JWT>"
    },
    {
      "key": "website_user_id",
      "value": "123"
    },
    {
      "key": "markup_group_id",
      "value": ""
    },
    {
      "key": "default_group_id",
      "value": ""
    },
    {
      "key": "markup_id",
      "value": ""
    }
  ],
  "item": [
    {
      "name": "1. Create / update markup (single API)",
      "item": [
        {
          "name": "Create markup (group + markups + agents)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"group_title\": \"B2B Silver\",\n  \"group_description\": \"Standard agents\",\n  \"is_default\": 0,\n  \"status\": 1,\n  \"markups\": [\n    {\n      \"title\": \"Silver - everything else\",\n      \"markup_amount_percentage\": 10,\n      \"discount_amount_percentage\": 0,\n      \"price_from\": 0,\n      \"price_to\": 99999999,\n      \"score\": 0,\n      \"status\": 1\n    },\n    {\n      \"title\": \"Silver - Bangkok peak\",\n      \"hotel_ids\": \"101,205\",\n      \"cities\": \"Bangkok\",\n      \"country\": \"Thailand\",\n      \"price_from\": 0,\n      \"price_to\": 99999999,\n      \"score\": 50,\n      \"booking_date_from\": \"2026-10-01\",\n      \"booking_date_to\": \"2026-12-31\",\n      \"travel_date_from\": \"2026-12-15\",\n      \"travel_date_to\": \"2027-01-05\",\n      \"markup_amount_percentage\": 12,\n      \"discount_amount_percentage\": 2,\n      \"status\": 1\n    }\n  ],\n  \"website_user_ids\": [\n    \"{{website_user_id}}\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/save_hotel_markup_group",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "save_hotel_markup_group"
              ]
            },
            "description": "ONE call, one transaction: creates the group, all its markups and (optionally) puts agents in it. If any part is invalid nothing is saved and the error names the row, e.g. 'markups[1]: price_from must be <= price_to'.\n\nRequired: group_title, markups (at least one), markup_amount_percentage on each markup. website_user_ids optional (group must be active). Response data = group with its markups (with ids); summary = counts."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));",
                  "if (j.data) {",
                  "  pm.collectionVariables.set('markup_group_id', j.data.id);",
                  "  if (j.data.markups && j.data.markups.length) pm.collectionVariables.set('markup_id', j.data.markups[0].id);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Create default markup group (single API)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"group_title\": \"Default\",\n  \"group_description\": \"Agents with no group, and requests with no agent\",\n  \"is_default\": 1,\n  \"status\": 1,\n  \"markups\": [\n    {\n      \"title\": \"Default - everything\",\n      \"markup_amount_percentage\": 10,\n      \"discount_amount_percentage\": 0,\n      \"price_from\": 0,\n      \"price_to\": 99999999,\n      \"score\": 0,\n      \"status\": 1\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/save_hotel_markup_group",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "save_hotel_markup_group"
              ]
            },
            "description": "Same API. is_default 1 makes this the fallback group (clears the flag on every other group). Create it once."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));",
                  "if (j.data) pm.collectionVariables.set('default_group_id', j.data.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "Update markup (group + markups)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"{{markup_group_id}}\",\n  \"group_title\": \"B2B Silver (updated)\",\n  \"markups\": [\n    {\n      \"id\": \"{{markup_id}}\",\n      \"markup_amount_percentage\": 9.5\n    },\n    {\n      \"title\": \"Silver - Pattaya\",\n      \"cities\": \"Pattaya\",\n      \"markup_amount_percentage\": 7,\n      \"price_to\": 99999999\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/save_hotel_markup_group",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "save_hotel_markup_group"
              ]
            },
            "description": "Same API with id = update. markups is the FULL list for the group: rows with id are updated (only the fields sent), rows without id are added, the group's other markups are deleted (status 2). Omit the markups key to change only the group fields."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "2. Markup groups",
      "item": [
        {
          "name": "List markup groups",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"page\": 1,\n  \"limit\": 20,\n  \"status\": \"\",\n  \"search\": \"\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/list_hotel_markup_groups",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "list_hotel_markup_groups"
              ]
            },
            "description": "Filters optional: status (0/1), search (title LIKE). Returns markup_count and agent_count per group."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        },
        {
          "name": "Get markup group (with markups)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"{{markup_group_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/get_hotel_markup_group",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "get_hotel_markup_group"
              ]
            },
            "description": "Group plus all its non-deleted markup conditions, highest score first."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        },
        {
          "name": "Update markup group",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"{{markup_group_id}}\",\n  \"group_title\": \"B2B Silver (updated)\",\n  \"status\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/update_hotel_markup_group",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "update_hotel_markup_group"
              ]
            },
            "description": "Send id plus only the fields to change: group_title, group_description, is_default, status."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "3. Markups (group conditions)",
      "item": [
        {
          "name": "List markups",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"markup_group_id\": \"{{markup_group_id}}\",\n  \"page\": 1,\n  \"limit\": 20,\n  \"status\": \"\",\n  \"search\": \"\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/list_hotel_markups",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "list_hotel_markups"
              ]
            },
            "description": "All filters optional."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        },
        {
          "name": "Get markup",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"{{markup_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/get_hotel_markup",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "get_hotel_markup"
              ]
            },
            "description": "One markup condition."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        },
        {
          "name": "Update markup",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"{{markup_id}}\",\n  \"markup_amount_percentage\": 9.5,\n  \"cities\": \"Bangkok,Pattaya\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/update_hotel_markup",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "update_hotel_markup"
              ]
            },
            "description": "Send id plus only the fields to change. Send null or \"\" to clear a list / date (= any)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        },
        {
          "name": "Delete markup",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"{{markup_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/delete_hotel_markup",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "delete_hotel_markup"
              ]
            },
            "description": "Soft delete (status = 2)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "4. Agent assignment",
      "item": [
        {
          "name": "Assign group to agents",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"website_user_ids\": [\n    \"{{website_user_id}}\"\n  ],\n  \"hotel_markup_group_id\": \"{{markup_group_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/assign_hotel_markup_group",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "assign_hotel_markup_group"
              ]
            },
            "description": "Sets fdk_holidays.website_users.hotel_markup_group_id. website_user_ids: array or comma string, max 500. The group must be active."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        },
        {
          "name": "Remove group from agent (use default)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"website_user_ids\": [\n    \"{{website_user_id}}\"\n  ],\n  \"hotel_markup_group_id\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/assign_hotel_markup_group",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "assign_hotel_markup_group"
              ]
            },
            "description": "hotel_markup_group_id null = unassign; the agent then gets the default group."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        },
        {
          "name": "Get agent's markup group",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"website_user_id\": \"{{website_user_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/get_agent_hotel_markup_group",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "get_agent_hotel_markup_group"
              ]
            },
            "description": "effective_group_id is the group pricing will use (assigned if active, else default)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        },
        {
          "name": "Preview markup for a price",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_id\": \"{{website_user_id}}\",\n  \"hotel_id\": \"101\",\n  \"city\": \"Bangkok\",\n  \"country\": \"Thailand\",\n  \"checkin\": \"2026-12-20\",\n  \"amount\": 25000,\n  \"currency\": \"THB\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/preview_hotel_markup",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "preview_hotel_markup"
              ]
            },
            "description": "Dry run of the pricing rule: which markup row applies and the amounts, in the given currency. Nothing is saved."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "5. Cleanup",
      "item": [
        {
          "name": "Delete markup group",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"{{markup_group_id}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/delete_hotel_markup_group",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "delete_hotel_markup_group"
              ]
            },
            "description": "Soft delete (status = 2). Agents still assigned fall back to the default group. Run last."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('status true', () => pm.expect(j.status).to.eql(true));"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}
