{
  "info": {
    "name": "Itland Attendance API v1",
    "_postman_id": "0f28a96d-42c9-458a-84fa-b6956af84fc3",
    "description": "API فقط‌خواندنی حضور و غیاب پنل آیتی‌لند.\n\nشروع سریع:\n1) کلید API را از پنل (کلیدهای API) بگیرید.\n2) در Environment مقدار apiKey و baseUrl را بگذارید.\n3) ابتدا «whoami» و سپس «همهٔ جلسه‌ها» را اجرا کنید؛ متغیرهای بقیهٔ درخواست‌ها خودکار پر می‌شوند.\n4) کل Collection را با Runner اجرا کنید تا همهٔ تست‌ها یک‌جا چک شوند.\n\nکلید در هدر X-Api-Key ارسال می‌شود (Authorization: Bearer هم پذیرفته می‌شود).",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "apikey",
    "apikey": [
      {
        "key": "key",
        "value": "X-Api-Key",
        "type": "string"
      },
      {
        "key": "value",
        "value": "{{apiKey}}",
        "type": "string"
      },
      {
        "key": "in",
        "value": "header",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "if (!pm.variables.get('apiKey') && !pm.environment.get('apiKey')) { console.warn('apiKey تنظیم نشده است؛ در Environment مقدار آن را بگذارید.'); }"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "pm.test('پاسخ JSON است', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));",
          "pm.test('زمان پاسخ کمتر از ۵ ثانیه', () => pm.expect(pm.response.responseTime).to.be.below(5000));"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://panel.itland.org"
    },
    {
      "key": "apiKey",
      "value": ""
    },
    {
      "key": "externalMeetingId",
      "value": "class42"
    },
    {
      "key": "date",
      "value": "2026-09-20"
    },
    {
      "key": "sessionId",
      "value": ""
    },
    {
      "key": "internalMeetingId",
      "value": ""
    }
  ],
  "item": [
    {
      "name": "0. اتصال و کلید",
      "item": [
        {
          "name": "whoami — تست کلید و مجوزها",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/whoami",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "whoami"
              ],
              "query": []
            },
            "description": "اولین درخواست بعد از دریافت کلید. نام موسسه، نام کلید، مجوزهای include و زمان سرور را برمی‌گرداند."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('نام موسسه و کلید برمی‌گردد', () => { pm.expect(j.institute).to.be.a('string').and.not.empty; pm.expect(j.keyName).to.be.a('string').and.not.empty; });",
                  "pm.test('پیشوند کلید با itl_ شروع می‌شود', () => pm.expect(j.keyPrefix).to.match(/^itl_/));",
                  "pm.test('پاسخ کش نمی‌شود', () => pm.expect(pm.response.headers.get('Cache-Control')).to.include('no-store'));",
                  "pm.test('کلید کامل هرگز در پاسخ تکرار نمی‌شود', () => { const k = pm.variables.get('apiKey'); if (k) pm.expect(pm.response.text()).to.not.include(k); });",
                  "console.log('IP مجاز: ' + j.canViewIpAddresses + ' | چت مجاز: ' + j.canViewChat);"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "1. فهرست جلسه‌ها",
      "item": [
        {
          "name": "همهٔ جلسه‌ها (تازه‌ترین اول) — متغیرهای بعدی را خودکار پر می‌کند",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings?page=1&pageSize=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "pageSize",
                  "value": "20"
                }
              ]
            },
            "description": "هر بار تشکیل کلاس یک ردیف مستقل است. این درخواست بعد از اجرا متغیرهای sessionId، internalMeetingId، externalMeetingId و date را از تازه‌ترین جلسه پر می‌کند تا بقیهٔ درخواست‌ها بدون ویرایش دستی کار کنند."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('قالب صفحه‌بندی', () => { pm.expect(j).to.have.all.keys('items', 'page', 'pageSize', 'totalCount', 'totalPages'); });",
                  "pm.test('هر جلسه شناسه و وضعیت دارد', () => j.items.forEach(s => { pm.expect(s.sessionId).to.be.a('string'); pm.expect(s.status).to.be.oneOf(['live', 'ended']); }));",
                  "pm.test('تازه‌ترین اول', () => { for (let i = 1; i < j.items.length; i++) pm.expect(new Date(j.items[i - 1].startedAt) >= new Date(j.items[i].startedAt)).to.be.true; });",
                  "if (j.items.length) { const s = j.items[0]; pm.collectionVariables.set('sessionId', s.sessionId); pm.collectionVariables.set('internalMeetingId', s.internalMeetingId); pm.collectionVariables.set('externalMeetingId', s.externalMeetingId); pm.collectionVariables.set('date', s.localDate); console.log('متغیرها از جلسهٔ «' + s.name + '» پر شد'); }"
                ]
              }
            }
          ]
        },
        {
          "name": "جلسه‌های یک کلاس (externalMeetingId)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings?externalMeetingId={{externalMeetingId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings"
              ],
              "query": [
                {
                  "key": "externalMeetingId",
                  "value": "{{externalMeetingId}}"
                }
              ]
            },
            "description": "اگر کلاس در یک روز دو بار تشکیل شده باشد، اینجا دو ردیف جدا می‌بینید."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('همه از همان کلاس‌اند', () => j.items.forEach(s => pm.expect(s.externalMeetingId).to.eql(pm.variables.get('externalMeetingId'))));"
                ]
              }
            }
          ]
        },
        {
          "name": "جلسه‌های یک روز (میلادی)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings?date={{date}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings"
              ],
              "query": [
                {
                  "key": "date",
                  "value": "{{date}}"
                }
              ]
            },
            "description": "روز به وقت تهران است. تاریخ شمسی هم پذیرفته می‌شود: date=1405-06-29"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('همه در همان روز تهرانی‌اند', () => j.items.forEach(s => pm.expect(s.localDate).to.eql(pm.variables.get('date'))));"
                ]
              }
            }
          ]
        },
        {
          "name": "بازه با from/to (زمان کامل UTC)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings?from={{date}}T00:00:00Z&to={{date}}T23:59:59Z",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{date}}T00:00:00Z"
                },
                {
                  "key": "to",
                  "value": "{{date}}T23:59:59Z"
                }
              ]
            },
            "description": "زمان کامل باید offset داشته باشد (Z یا +03:30). اگر از +03:30 استفاده می‌کنید در URL باید %2B نوشته شود. تاریخ تنها (بدون ساعت) هم مجاز است و به‌معنای روز تهرانی است."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "فقط جلسه‌های در حال برگزاری",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings?status=live",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "live"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('همه live', () => j.items.forEach(s => pm.expect(s.status).to.eql('live')));"
                ]
              }
            }
          ]
        },
        {
          "name": "با پیشوند شناسهٔ کلاس (برای شناسه‌های یکتا به‌ازای هر جلسه)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings?externalMeetingIdPrefix=class42-",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings"
              ],
              "query": [
                {
                  "key": "externalMeetingIdPrefix",
                  "value": "class42-"
                },
                {
                  "key": "status",
                  "value": "ended",
                  "disabled": true
                },
                {
                  "key": "courseCode",
                  "value": "MATH101",
                  "disabled": true,
                  "description": "کد درس تعریف‌شده در پنل (اختیاری)"
                }
              ]
            },
            "description": "مثلاً اگر شناسه‌ها class42-2026-09-20-am و class42-2026-09-20-pm هستند."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "2. جزئیات یک جلسه",
      "item": [
        {
          "name": "با sessionId",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings/{{sessionId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings",
                "{{sessionId}}"
              ],
              "query": []
            },
            "description": "حاضرین، همهٔ بازه‌های ورود/خروج، مدت واقعی حضور و درصد حضور نسبت به پرحضورترین فرد (معمولاً استاد)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('فیلدهای اصلی جلسه', () => { ['sessionId', 'internalMeetingId', 'externalMeetingId', 'status', 'startedAt', 'localDate', 'localDateJalali', 'sequenceInDay', 'sessionsInDay', 'participants'].forEach(k => pm.expect(j).to.have.property(k)); });",
                  "pm.test('پیش‌فرض: بدون آی‌پی و چت', () => { pm.expect(j).to.not.have.property('chat'); j.participants.forEach(p => { pm.expect(p).to.not.have.property('ipAddresses'); p.intervals.forEach(i => pm.expect(i).to.not.have.property('ipAddress')); }); });",
                  "pm.test('درصد حضور بین ۰ تا ۱۰۰', () => j.participants.forEach(p => pm.expect(p.attendancePercent).to.be.within(0, 100)));",
                  "pm.test('مدت حضور = اجتماع بازه‌ها (بیشتر از طول جلسه نیست)', () => j.participants.forEach(p => pm.expect(p.totalPresenceSeconds).to.be.at.most(j.durationSeconds + 60)));",
                  "pm.test('پرحضورترین فرد ۱۰۰٪ است', () => { if (j.participants.length) pm.expect(Math.max(...j.participants.map(p => p.attendancePercent))).to.eql(100); });"
                ]
              }
            }
          ]
        },
        {
          "name": "با internalMeetingId بیگ‌بلو",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings/{{internalMeetingId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings",
                "{{internalMeetingId}}"
              ],
              "query": []
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('همان جلسه', () => pm.expect(j.sessionId).to.eql(pm.variables.get('sessionId')));"
                ]
              }
            }
          ]
        },
        {
          "name": "با آستانهٔ حضور دلخواه (threshold)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings/{{sessionId}}?threshold=85",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings",
                "{{sessionId}}"
              ],
              "query": [
                {
                  "key": "threshold",
                  "value": "85"
                }
              ]
            },
            "description": "belowThreshold برای کسانی true می‌شود که درصدشان کمتر از این عدد باشد (پیش‌فرض ۷۰)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('آستانه اعمال شد', () => { pm.expect(j.thresholdPercent).to.eql(85); j.participants.forEach(p => pm.expect(p.belowThreshold).to.eql(p.attendancePercent < 85)); });"
                ]
              }
            }
          ]
        },
        {
          "name": "با آی‌پی و چت (نیازمند مجوز کلید)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings/{{sessionId}}?include=ip,chat",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings",
                "{{sessionId}}"
              ],
              "query": [
                {
                  "key": "include",
                  "value": "ip,chat"
                }
              ]
            },
            "description": "اگر کلید مجوز نداشته باشد ۴۰۳ با کد forbidden_include برمی‌گردد. مجوز را مدیر پنل هنگام ساخت کلید می‌دهد."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('۲۰۰ (با مجوز) یا ۴۰۳ forbidden_include (بدون مجوز)', () => { if (pm.response.code === 403) { pm.expect(pm.response.json().error.code).to.eql('forbidden_include'); } else { pm.response.to.have.status(200); } });",
                  "if (pm.response.code === 200) { const j = pm.response.json(); pm.test('چت و آی‌پی در پاسخ است', () => { pm.expect(j).to.have.property('chat'); j.participants.forEach(p => pm.expect(p).to.have.property('ipAddresses')); }); }"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "3. کلاس با چند جلسه (مثلاً دو بار در روز)",
      "item": [
        {
          "name": "جلسه‌های یک کلاس در یک روز — هر جلسه جدا",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/classes/{{externalMeetingId}}/sessions?date={{date}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "classes",
                "{{externalMeetingId}}",
                "sessions"
              ],
              "query": [
                {
                  "key": "date",
                  "value": "{{date}}"
                }
              ]
            },
            "description": "همهٔ جلسه‌های آن روز را با جزئیات کامل و به ترتیب زمان برمی‌گرداند. sequenceInDay/sessionsInDay می‌گوید «جلسهٔ چندم از چند جلسهٔ آن روز»."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('قالب صفحه‌بندی', () => pm.expect(j).to.have.property('items'));",
                  "pm.test('به ترتیب زمان شروع', () => { for (let i = 1; i < j.items.length; i++) pm.expect(new Date(j.items[i - 1].startedAt) <= new Date(j.items[i].startedAt)).to.be.true; });",
                  "pm.test('شماره جلسه در روز درست است', () => { const byDay = {}; j.items.forEach(s => (byDay[s.localDate] = byDay[s.localDate] || []).push(s)); Object.values(byDay).forEach(list => list.forEach((s, i) => { pm.expect(s.sequenceInDay).to.eql(i + 1); pm.expect(s.sessionsInDay).to.be.at.least(list.length); })); });",
                  "pm.test('هر جلسه شناسهٔ یکتای خودش را دارد', () => pm.expect(new Set(j.items.map(s => s.sessionId)).size).to.eql(j.items.length));",
                  "console.log('این کلاس در این روز ' + j.totalCount + ' جلسه دارد');"
                ]
              }
            }
          ]
        },
        {
          "name": "با تاریخ شمسی",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/classes/{{externalMeetingId}}/sessions?date=1405-06-29",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "classes",
                "{{externalMeetingId}}",
                "sessions"
              ],
              "query": [
                {
                  "key": "date",
                  "value": "1405-06-29"
                }
              ]
            },
            "description": "تاریخ شمسی را با مقدار روز خودتان جایگزین کنید."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200 یا 404 اگر کلاس ناشناخته است', () => pm.expect(pm.response.code).to.be.oneOf([200, 404]));"
                ]
              }
            }
          ]
        },
        {
          "name": "با بازهٔ from/to",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/classes/{{externalMeetingId}}/sessions?from={{date}}&to={{date}}&threshold=70",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "classes",
                "{{externalMeetingId}}",
                "sessions"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{date}}"
                },
                {
                  "key": "to",
                  "value": "{{date}}"
                },
                {
                  "key": "threshold",
                  "value": "70"
                },
                {
                  "key": "include",
                  "value": "ip,chat",
                  "disabled": true,
                  "description": "اختیاری؛ نیازمند مجوز کلید"
                }
              ]
            },
            "description": "from/to اگر تاریخ تنها باشند روز تهرانی‌اند و to کل آن روز را شامل می‌شود."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "جمع‌بندی حضور هر فرد در همهٔ جلسه‌های بازه",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/classes/{{externalMeetingId}}/attendance?from={{date}}&to={{date}}&threshold=70",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "classes",
                "{{externalMeetingId}}",
                "attendance"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{date}}"
                },
                {
                  "key": "to",
                  "value": "{{date}}"
                },
                {
                  "key": "threshold",
                  "value": "70"
                }
              ]
            },
            "description": "برای هر فرد: تعداد جلسه‌های حاضر/غایب، میانگین درصد (غیبت = ۰٪)، مجموع ثانیه حضور و جزئیات هر جلسه. برای یک ترم، from/to را روی کل ترم بگذارید."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const j = pm.response.json();",
                  "pm.test('sessionCount با آرایهٔ sessions یکی است', () => pm.expect(j.sessionCount).to.eql(j.sessions.length));",
                  "pm.test('حاضر + غایب = تعداد جلسه‌ها', () => j.participants.forEach(p => pm.expect(p.sessionsAttended + p.sessionsAbsent).to.eql(j.sessionCount)));",
                  "pm.test('میانگین بین ۰ تا ۱۰۰', () => j.participants.forEach(p => pm.expect(p.averagePercent).to.be.within(0, 100)));",
                  "pm.test('جزئیات هر جلسه به جلسه‌های بازه اشاره می‌کند', () => { const ids = new Set(j.sessions.map(s => s.sessionId)); j.participants.forEach(p => p.sessions.forEach(s => pm.expect(ids.has(s.sessionId)).to.be.true)); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "4. خطاها (برای اطمینان از رفتار درست)",
      "item": [
        {
          "name": "بدون کلید → 401 missing_api_key",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/whoami",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "whoami"
              ],
              "query": []
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 401', () => pm.response.to.have.status(401));",
                  "pm.test('کد خطا missing_api_key', () => pm.expect(pm.response.json().error.code).to.eql('missing_api_key'));",
                  "pm.test('پیام فارسی دارد', () => pm.expect(pm.response.json().error.message).to.be.a('string').and.not.empty);"
                ]
              }
            }
          ]
        },
        {
          "name": "کلید نامعتبر → 401 invalid_api_key",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/whoami",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "whoami"
              ],
              "query": []
            },
            "auth": {
              "type": "apikey",
              "apikey": [
                {
                  "key": "key",
                  "value": "X-Api-Key",
                  "type": "string"
                },
                {
                  "key": "value",
                  "value": "itl_thisKeyDoesNotExist0000000000000000000",
                  "type": "string"
                },
                {
                  "key": "in",
                  "value": "header",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 401', () => pm.response.to.have.status(401));",
                  "pm.test('کد خطا invalid_api_key', () => pm.expect(pm.response.json().error.code).to.eql('invalid_api_key'));",
                  "pm.test('پیام فارسی دارد', () => pm.expect(pm.response.json().error.message).to.be.a('string').and.not.empty);"
                ]
              }
            }
          ]
        },
        {
          "name": "کلید در query-string پذیرفته نمی‌شود → 401",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/whoami?api_key={{apiKey}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "whoami"
              ],
              "query": [
                {
                  "key": "api_key",
                  "value": "{{apiKey}}"
                }
              ]
            },
            "description": "عمداً فقط هدر پذیرفته می‌شود چون آدرس‌ها در لاگ سرورها می‌مانند.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 401', () => pm.response.to.have.status(401));",
                  "pm.test('کد خطا missing_api_key', () => pm.expect(pm.response.json().error.code).to.eql('missing_api_key'));",
                  "pm.test('پیام فارسی دارد', () => pm.expect(pm.response.json().error.message).to.be.a('string').and.not.empty);"
                ]
              }
            }
          ]
        },
        {
          "name": "کلاس ناشناخته → 404 class_not_found",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/classes/no-such-class-xyz/sessions?date=2026-01-01",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "classes",
                "no-such-class-xyz",
                "sessions"
              ],
              "query": [
                {
                  "key": "date",
                  "value": "2026-01-01"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 404', () => pm.response.to.have.status(404));",
                  "pm.test('کد خطا class_not_found', () => pm.expect(pm.response.json().error.code).to.eql('class_not_found'));",
                  "pm.test('پیام فارسی دارد', () => pm.expect(pm.response.json().error.message).to.be.a('string').and.not.empty);"
                ]
              }
            }
          ]
        },
        {
          "name": "جلسهٔ ناشناخته → 404 session_not_found",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings/00000000-0000-0000-0000-000000000000",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings",
                "00000000-0000-0000-0000-000000000000"
              ],
              "query": []
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 404', () => pm.response.to.have.status(404));",
                  "pm.test('کد خطا session_not_found', () => pm.expect(pm.response.json().error.code).to.eql('session_not_found'));",
                  "pm.test('پیام فارسی دارد', () => pm.expect(pm.response.json().error.message).to.be.a('string').and.not.empty);"
                ]
              }
            }
          ]
        },
        {
          "name": "بدون date/from → 400 missing_parameter",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/classes/{{externalMeetingId}}/sessions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "classes",
                "{{externalMeetingId}}",
                "sessions"
              ],
              "query": []
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 400', () => pm.response.to.have.status(400));",
                  "pm.test('کد خطا missing_parameter', () => pm.expect(pm.response.json().error.code).to.eql('missing_parameter'));",
                  "pm.test('پیام فارسی دارد', () => pm.expect(pm.response.json().error.message).to.be.a('string').and.not.empty);"
                ]
              }
            }
          ]
        },
        {
          "name": "زمان بدون offset → 400 invalid_parameter",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings?from=2026-09-20T08:00:00",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "2026-09-20T08:00:00"
                }
              ]
            },
            "description": "بدون offset صریح، زمان مبهم است (تهران یا UTC؟) و رد می‌شود."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 400', () => pm.response.to.have.status(400));",
                  "pm.test('کد خطا invalid_parameter', () => pm.expect(pm.response.json().error.code).to.eql('invalid_parameter'));",
                  "pm.test('پیام فارسی دارد', () => pm.expect(pm.response.json().error.message).to.be.a('string').and.not.empty);"
                ]
              }
            }
          ]
        },
        {
          "name": "بازهٔ معکوس → 400 invalid_range",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings?from=2026-09-21&to=2026-09-20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "2026-09-21"
                },
                {
                  "key": "to",
                  "value": "2026-09-20"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 400', () => pm.response.to.have.status(400));",
                  "pm.test('کد خطا invalid_range', () => pm.expect(pm.response.json().error.code).to.eql('invalid_range'));",
                  "pm.test('پیام فارسی دارد', () => pm.expect(pm.response.json().error.message).to.be.a('string').and.not.empty);"
                ]
              }
            }
          ]
        },
        {
          "name": "threshold نامعتبر → 400",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings/{{sessionId}}?threshold=150",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings",
                "{{sessionId}}"
              ],
              "query": [
                {
                  "key": "threshold",
                  "value": "150"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 400', () => pm.response.to.have.status(400));",
                  "pm.test('کد خطا invalid_parameter', () => pm.expect(pm.response.json().error.code).to.eql('invalid_parameter'));",
                  "pm.test('پیام فارسی دارد', () => pm.expect(pm.response.json().error.message).to.be.a('string').and.not.empty);"
                ]
              }
            }
          ]
        },
        {
          "name": "include ناشناخته → 400",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/meetings/{{sessionId}}?include=passwords",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "meetings",
                "{{sessionId}}"
              ],
              "query": [
                {
                  "key": "include",
                  "value": "passwords"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 400', () => pm.response.to.have.status(400));",
                  "pm.test('کد خطا invalid_parameter', () => pm.expect(pm.response.json().error.code).to.eql('invalid_parameter'));",
                  "pm.test('پیام فارسی دارد', () => pm.expect(pm.response.json().error.message).to.be.a('string').and.not.empty);"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}
