# Internal API — คู่มือสำหรับ chat-v2 (Node.js)

> API สำหรับ server-to-server เท่านั้น — ห้าม expose token หรือ secret ไปฝั่ง client

---

## Auth

```
Base URL : {APP_URL}/api/internal
Header   : Authorization: Bearer {sanctum-token}
           Accept: application/json
```

ออก token:

```bash
php artisan chat:issue-token
```

เก็บ plain-text token ใน env ฝั่ง Node.js เช่น `INTERNAL_API_KEY`

---

## สรุป Endpoints

| Method | Path | ใช้ทำอะไร |
|--------|------|-----------|
| GET | `/pharmacy/products` | ดึง catalog สินค้าทั้งหมด (paginate) |
| GET | `/pharmacy/products/changes` | ดึงสินค้าที่เปลี่ยนหลัง `since` |
| POST | `/pharmacy/availability` | เช็ค stock / ราคา / URL ต่อสินค้า |
| GET | `/settings/line` | ดึง LINE config จากหลังบ้าน |
| POST | `/member/line-user-id/check` | เช็คว่า LINE user id ถูกใช้เป็น member แล้วหรือยัง |
| POST | `/member/uid/check` | หา member ด้วย uid (chat-v2 handoff) |

---

## 1) Catalog — `GET /pharmacy/products`

### Query

| Param | Default | หมายเหตุ |
|-------|---------|----------|
| `page` | 1 | |
| `per_page` | 500 | max ตาม `CHAT_CATALOG_PER_PAGE_MAX` |
| `active_only` | true | กรองเฉพาะสินค้า/หมวด active |

### ตัวอย่าง Request

```
GET /api/internal/pharmacy/products?page=1&per_page=100
Authorization: Bearer {token}
```

### ตัวอย่าง Response

```json
{
  "success": true,
  "page": 1,
  "per_page": 100,
  "total": 250,
  "since": null,
  "items": [
    {
      "id": 123,
      "uid": "uuid",
      "sku": "MED-001",
      "image": "https://example.com/storage/products/abc.jpg",
      "category": {
        "id": 5,
        "langs": [
          { "lang": "th", "name": "ยาแก้ปวด" },
          { "lang": "en", "name": "Analgesics" }
        ]
      },
      "status": 1,
      "updated_at": "2026-06-09T12:00:00+00:00",
      "langs": [
        {
          "lang": "th",
          "name": "พาราเซตามอล 500 มก.",
          "manufacturer": "บริษัท ยา จำกัด",
          "desc": "...",
          "size": "",
          "unit": "เม็ด",
          "information": "",
          "component": "Paracetamol 500mg",
          "indication": "",
          "take": "",
          "storage": ""
        }
      ],
      "tags": [
        {
          "id": 1,
          "sku": "TAG-01",
          "langs": [
            { "lang": "th", "name": "ยาแก้ปวด" }
          ]
        }
      ],
      "member_types": ["general", "wholesale", "wholesale_individual", "wholesale_registration", "wholesale_corporation"],
      "member_type_ids": [1, 2, 4, 5, 6]
    }
  ],
  "deleted_ids": []
}
```

> catalog **ไม่มี** `stock`, `price`, `discount` — ใช้ availability แยก
>
> `member_types` / `member_type_ids` ถ้าเป็น `null`, `[]`, ไม่มี field, หรือว่าง ให้ตีความว่าใช้ได้ทุก member type และใช้เป็น coarse filter เท่านั้น; การตัดสินสุดท้ายยังอิง Availability API
>
> สินค้าเลือกได้แค่ parent แต่ payload ขยายรวม child ของ parent ที่ติดสินค้าด้วย (เช่น `wholesale` → leaf SKUs/IDs) เพื่อให้ตรงกับ member ที่ใช้ type ปลายสาย

---

## 2) Delta Catalog — `GET /pharmacy/products/changes`

### Query

| Param | Required | หมายเหตุ |
|-------|----------|----------|
| `since` | yes | ISO8601 เช่น `2026-06-09T00:00:00Z` |
| `page` | no | default 1 |
| `per_page` | no | default 500 |

### ตัวอย่าง Request

```
GET /api/internal/pharmacy/products/changes?since=2026-06-09T00:00:00Z
Authorization: Bearer {token}
```

### ตัวอย่าง Response

```json
{
  "success": true,
  "page": 1,
  "per_page": 500,
  "total": 3,
  "since": "2026-06-09T00:00:00+00:00",
  "items": [ "... catalog item shape เหมือนข้อ 1 ..." ],
  "deleted_ids": [ 45, 67 ]
}
```

---

## 3) Availability — `POST /pharmacy/availability`

### Request body

```json
{
  "member_type": "general",
  "member_uid": "member-uuid",
  "items": [
    { "id": 123 },
    { "id": 456 }
  ]
}
```

| Field | หมายเหตุ |
|-------|----------|
| `member_type` | `member_types.sku` — parent หรือ child ก็ได้ เช่น `general`, `wholesale`, `pharmacy`, `wholesale_individual` (default: `general`) |
| `member_uid` | `member_members.uid` (optional; ถ้าส่งมาจะใช้ `type_id` ของ member แทน) |
| `items[].id` | `product_products.id` (required, max ตาม `CHAT_AVAILABILITY_MAX_ITEMS`) |

### ตัวอย่าง Response

```json
{
  "success": true,
  "items": [
    {
      "id": 123,
      "stock": 5,
      "status": 1,
      "product_url": "https://example.com/th/paracetamol-500mg",
      "product_image": "https://example.com/uploads/product/paracetamol.jpg",
      "member_type_id": 1,
      "price": 45.0,
      "discount": 7.0,
      "discount_type": "BAHT"
    },
    {
      "id": 456,
      "stock": null,
      "status": null,
      "product_url": null,
      "product_image": null,
      "member_type_id": null,
      "price": null,
      "discount": null,
      "discount_type": null
    }
  ]
}
```

> ถ้าไม่พบสินค้า: คืน `id` + ฟิลด์อื่นเป็น `null`  
> `product_image` = full URL รูปสินค้าจาก `check_file(product_products.image)` — fallback เป็นรูป default ถ้าไม่มีไฟล์  
> คำนวณราคาสุทธิฝั่ง Node.js จาก `price`, `discount`, `discount_type` (`BAHT` / `PERCENTAGE`)

---

## 4) LINE Config — `GET /settings/line`

ดึงค่า LINE จากหลังบ้าน Laravel (Settings → LINE) ใช้ตอน setup / เชื่อม admin กับ LINE OA

### ตัวอย่าง Request

```
GET /api/internal/settings/line
Authorization: Bearer {token}
```

### ตัวอย่าง Response

```json
{
  "success": true,
  "line_message_channel_id": "1234567890",
  "line_message_channel_secret": "xxxxxxxx",
  "line_access_token": "Channel access token (Messaging API)",
  "line_liff_id": "200xxxxx-xxxxxxxx",
  "line_login_channel_id": "1234567890",
  "line_login_channel_secret": "xxxxxxxx",
  "app_url": "https://themedicine.example.com"
}
```

| Field | ใช้ทำอะไรฝั่ง Node.js |
|-------|----------------------|
| `line_message_channel_id` | Messaging API Channel ID |
| `line_message_channel_secret` | Messaging API Channel Secret |
| `line_access_token` | เรียก Messaging API (push / multicast) |
| `line_liff_id` | เปิด LIFF ในแอป LINE |
| `line_login_channel_id` | LINE Login OAuth |
| `line_login_channel_secret` | แลก code / verify id_token |
| `app_url` | domain ที่ใช้เรียก API นี้ (จาก request) — ใช้สร้าง callback / redirect URI |

---

## 5) เช็ค Member ซ้ำ — `POST /member/line-user-id/check`

ใช้ก่อนบันทึกว่า admin เชื่อม LINE สำเร็จ — ถ้า LINE account นี้เป็น member แล้ว ห้ามใช้เป็น admin

### Request body

```json
{
  "line_user_id": "Uabc123..."
}
```

### ตัวอย่าง Response — ไม่พบ

```json
{
  "success": true,
  "found": false,
  "member": null
}
```

→ เชื่อม admin ได้

### ตัวอย่าง Response — พบ member

```json
{
  "success": true,
  "found": true,
  "member": {
    "id": 5,
    "uid": "member-uuid",
    "type_id": 1,
    "member_type": "general",
    "store_name": "ร้าน ABC",
    "store_type": null,
    "name": "สมชาย",
    "tel": "0812345678",
    "line_id": "@somchai",
    "line_user_id": "Uabc123...",
    "email": "somchai@example.com",
    "status": 1,
    "approved": "APPROVED",
    "updated_at": "2026-06-10T12:00:00+00:00"
  }
}
```

→ ห้ามใช้ LINE account นี้เป็น admin

### ตัวอย่าง Response — validation error (422)

```json
{
  "success": false,
  "message": "line_user_id is required",
  "errors": {
    "line_user_id": ["line_user_id is required"]
  }
}
```

---

## 6) หา Member ด้วย uid — `POST /member/uid/check`

ใช้ในหน้า handoff ของ chat-v2 — เมื่อมี `conversation.externalUserId` (= `member_members.uid`) ส่งมาเพื่อระบุว่าลูกค้าคือ member คนไหน

> ต่างจากเส้น `line-user-id/check`: เส้นนี้ **คืน member แม้ยัง inactive / ไม่ approved** เพื่อให้ staff เห็นว่าเป็นใครเสมอ

### Request body

```json
{
  "uid": "member-uuid"
}
```

### ตัวอย่าง Response — พบ member

```json
{
  "success": true,
  "found": true,
  "member": {
    "id": 5,
    "uid": "member-uuid",
    "type_id": 1,
    "member_type": "general",
    "store_name": "ร้าน ABC",
    "store_type": null,
    "name": "สมชาย",
    "tel": "0812345678",
    "line_id": "@somchai",
    "line_user_id": "Uabc123...",
    "email": "somchai@example.com",
    "status": 1,
    "approved": "APPROVED",
    "updated_at": "2026-06-10T12:00:00+00:00"
  }
}
```

### ตัวอย่าง Response — ไม่พบ

```json
{
  "success": true,
  "found": false,
  "member": null
}
```

→ chat-v2 fallback แสดงข้อมูลคำขอเดิม

### ตัวอย่าง Response — validation error (422)

```json
{
  "success": false,
  "message": "uid is required",
  "errors": {
    "uid": ["uid is required"]
  }
}
```

`member_type` = `member_types.sku` (`general` / `wholesale` / `pharmacy`) — chat-v2 map เป็นไทยเอง

---

## Webhook (Laravel → chat-v2)

Laravel ส่ง webhook มาที่ chat-v2 เมื่อสินค้าเปลี่ยน (ไม่ใช่ endpoint ที่ Node เรียก Laravel)

```json
{
  "event": "product.updated",
  "source": "laravel",
  "content_type": "pharmacy_product",
  "id": 123,
  "payload": { "... catalog item shape ..." }
}
```

`product.deleted` → ตั้ง `status: 0` ใน payload

`payload.barcode` / `payload.qr_code` เป็น full URL ของรูป — chat-v2 เป็นฝ่ายเรียก AI ถอดเป็น text ตอน ingest แล้วใช้เทียบตอนลูกค้าส่งรูป barcode/QR ในแชท (`lookup_mode=code`); รูปฉลากใช้ชื่อยา (`lookup_mode=name`); อาการใช้ skill/agentPrompt กำหนดขอบเขตคำตอบ (รายละเอียดใน `docs/pharmacy-chat-internal-api.md`)

---

## Env ที่เกี่ยวข้อง

### ฝั่ง Laravel

```env
CHAT_API_URL=https://the-medicine-chatbot-docker.nd.co.th
CHAT_WEBHOOK_EMBED_URL=https://the-medicine-chatbot-docker.nd.co.th/api/external/webhook/laravel
CHAT_WEBHOOK_SECRET=
CHAT_CATALOG_PER_PAGE_MAX=500
CHAT_AVAILABILITY_MAX_ITEMS=50
```

### ฝั่ง Node.js (แนะนำ)

```env
LARAVEL_API_URL=http://127.0.0.1:8000
INTERNAL_API_KEY=1|xxxxxxxx...
```

---

## HTTP Status

| Status | ความหมาย |
|--------|----------|
| 200 | สำเร็จ |
| 401 | token ไม่ถูกต้อง / ไม่ได้ส่ง Authorization |
| 404 | path ผิด |
| 422 | request body ไม่ครบหรือไม่ถูกต้อง |

---

## Flow เชื่อม Admin กับ LINE (สรุป)

1. `GET /settings/line` → ดึง LINE config
2. ทำ LINE Login / LIFF ฝั่ง Node.js
3. ได้ `line_user_id` แล้ว → `POST /member/line-user-id/check`
4. `found: false` → บันทึก admin connection
5. `found: true` → แจ้ง admin ว่า LINE นี้เป็น member แล้ว

---

*อัปเดตล่าสุด: 2026-06-10*
