# Pharmacy Chat Internal API — คู่มือสำหรับ chat-v2

> Catalog ส่งชื่อฟิลด์ตรงกับตาราง Laravel; ส่วน Availability เพิ่มฟิลด์คำนวณ `visible` (สิทธิ์ตาม member type) เพื่อให้ฝั่ง chat กรอง card ได้

---

## ตารางที่เกี่ยวข้อง

| ตาราง | ใช้ใน |
|-------|--------|
| `product_products` | catalog + availability (`id`, `stock`, `status` ฯลฯ) |
| `product_products_translate` | catalog `langs[]` |
| `product_tags` + `product_tags_translate` | catalog `tags[]` |
| `product_products_price` | availability (`price`, `discount`, `discount_type`, `member_type_id`) |
| `member_members` | `member_uid` → `uid` |
| `member_types` | `member_type`: parent หรือ child SKU ใน `member_types.sku` |

---

## Base URL & Auth

```
{APP_URL}/api/internal/pharmacy
Authorization: Bearer {sanctum-token}
```

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

---

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

### Query

| Param | Default |
|-------|---------|
| `page` | 1 |
| `per_page` | 500 (max `CHAT_CATALOG_PER_PAGE_MAX`) |
| `active_only` | true |

### Item shape (`product_products` + relations)

```json
{
  "id": 123,
  "uid": "uuid",
  "sku": "MED-001",
  "image": "https://example.com/storage/products/abc.jpg",
  "barcode": "https://example.com/storage/product/list/barcode-xxx.png",
  "qr_code": "https://example.com/storage/product/list/qr_code-xxx.png",
  "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": ""
    },
    {
      "lang": "en",
      "name": "Paracetamol 500mg",
      "manufacturer": "Pharma Co.",
      "desc": "...",
      "size": "",
      "unit": "tablet",
      "information": "",
      "component": "Paracetamol 500mg",
      "indication": "",
      "take": "",
      "storage": ""
    }
  ],
  "tags": [
    {
      "id": 1,
      "sku": "TAG-01",
      "langs": [
        { "lang": "th", "name": "ยาแก้ปวด" },
        { "lang": "en", "name": "Analgesic" }
      ]
    }
  ],
  "member_types": ["general", "wholesale", "wholesale_individual", "wholesale_registration", "wholesale_corporation"],
  "member_type_ids": [1, 2, 4, 5, 6]
}
```

- `image` — full URL ผ่าน `check_file()` (fallback placeholder ถ้าไฟล์ไม่มี)
- `barcode` / `qr_code` — full URL ของรูป (ผ่าน `check_file()`) เมื่อมีค่า; ว่าง `""` ถ้าไม่มีรูป — **Laravel ส่งเฉพาะ URL ไม่ถอด text**
- `category` — `null` ถ้าไม่มีหมวดหมู่; ไม่ส่ง `category_id` แยก
- `member_types` / `member_type_ids` ถ้าเป็น `null`, `[]`, ไม่มี field, หรือว่าง ให้ตีความว่าใช้ได้ทุก member type; ใช้เป็น coarse filter ก่อนเรียก availability เท่านั้น
- สินค้าใน admin เลือกได้แค่ parent (`general` / `wholesale` / `pharmacy`) แต่ payload **ขยายรวม child ของแต่ละ parent** ด้วย (เช่น `wholesale` → รวม `wholesale_individual`, `wholesale_registration`, `wholesale_corporation`) เพื่อให้ค้นหา/กรองตรงกับ `member.type_id` ที่เป็น leaf

**ไม่มีใน export:** `stock`, `price`, `discount`, `gallery`, `weight`, `hot` (อยู่ใน DB แต่ไม่ส่งใน catalog ตาม spec)

### Chat-v2: barcode / QR → text (ฝั่ง AI)

Laravel ไม่เรียก AI อ่าน barcode/QR — หน้าที่นี้เป็นของ **chat-v2**

สัญญาฟิลด์ใน payload **อิง Laravel API**:
- `barcode` / `qr_code` = URL รูป (เหมือนที่ Laravel ส่งมา) — ไม่เปลี่ยนชื่อ ไม่สร้าง `barcode_text` / `qr_text`

ตอน ingest / embed `pharmacy_product` (webhook หรือ catalog sync):

1. ถ้า `payload.barcode` หรือ `payload.qr_code` เป็น URL ที่ไม่ว่าง → เรียก AI vision ถอดเป็น text
2. เก็บค่าที่ถอดได้ใน `_chat_v2_ocr.barcode` / `_chat_v2_ocr.qr_code` (internal ของ chat-v2 เท่านั้น)
3. เมื่อ profile เลือกฟิลด์ `barcode` / `qr_code` สำหรับ embed/search → ใช้ค่าที่ถอดได้ (ไม่เอา URL ไปฝังใน text)
4. ถ้า URL ไม่เปลี่ยนและมีค่าถอดเดิมแล้ว → reuse (ไม่เรียก AI ซ้ำ)
5. ตอนแชท:
   - รูป barcode/QR → `lookup_mode=code` เทียบกับค่าที่ถอดจาก catalog
   - รูปฉลากยา → `lookup_mode=name` ค้นด้วยชื่อยา
   - หลังเจอสินค้าแล้ว ถามรายละเอียดต่อได้จากฟิลด์ `langs[]` ใน catalog
   - อาการ: ขอบเขตคำตอบ/แนะนำถูกกำหนดโดย skill `symptom_inquiry` + agentPrompt override (เปิดแนะนำหรือห้ามตอบก็ได้)

ถ้าถอดไม่ได้หรือ URL ว่าง → ไม่มี text สำหรับค้นหา และไม่บล็อกการ sync สินค้า

> หมายเหตุ: ต้องมี AI image process config (type=3) ใน admin และอัปเดต embed profile ให้รวม `barcode`, `qr_code`, `indication`, `desc` ใน text search / embed fields (seed: `seed-external-embed-profiles.ts`)

---

## 2) Delta — `GET /products/changes?since={ISO8601}`

Response เหมือน full sync + `deleted_ids` (array ของ `product_products.id` ที่ soft-delete หลัง `since`)

---

## 3) Availability — `POST /availability`

### Request

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

| Field | ตาราง |
|-------|--------|
| `member_type` | `member_types.sku` (รับทั้ง parent และ child เช่น `wholesale_individual`) |
| `member_uid` | `member_members.uid` |
| `items[].id` | `product_products.id` |

### Response ต่อ item (เฉพาะคอลัมน์จริง)

```json
{
  "id": 123,
  "visible": true,
  "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"
}
```

| Field | ตาราง / แหล่งที่มา |
|-------|--------|
| `id`, `stock`, `status` | `product_products` |
| `visible` | คำนวณจากสิทธิ์ member type (ดูด้านล่าง) — `true` = member นี้เห็น/ซื้อสินค้าได้ |
| `member_type_id`, `price`, `discount`, `discount_type` | `product_products_price` (tier ที่เลือก) |
| `product_url` | full URL จาก `SlugController::getRoute('product', 'product_list', id)` — `null` ถ้าไม่มี slug |
| `product_image` | full URL รูปสินค้าจาก `check_file(product_products.image)` — fallback เป็นรูป default ถ้าไม่มีไฟล์ |

### `visible` คำนวณยังไง (ตรงกับ logic หน้าเว็บ)

ใช้กฎเดียวกับ `Modules\Product\app\Support\ProductPricing::isVisible()`:

1. `status` ของสินค้าต้อง = 1 และหมวดหมู่ต้อง active (`status` = 1) ไม่งั้น `visible: false`
2. ถ้าสินค้ามีการกำหนด `member_type` (ผูกไว้ที่สินค้าโดยตรง) → `visible: true` เฉพาะเมื่อ member type ตรงกัน
3. ถ้าสินค้าไม่ได้กำหนด `member_type` → เช็คจากหมวดหมู่ที่ถูกจำกัด: ถ้าสินค้าอยู่ในหมวดหมู่ (หรือหมวดลูก) ที่จำกัด type อื่น → `visible: false`, นอกนั้น `true`

> หมายเหตุ wholesale subtype: member type `4, 5, 6` ใช้ราคา tier `wholesale` (id `2`) แต่การเช็ค `visible` ใช้ `type_id` จริงของ member

**ฝั่งสร้าง card ควรกรองด้วย `visible: true` ก่อนแสดงสินค้า** (และ/หรือเช็ค `status` / `stock` ตามต้องการ)

**ตัดออกแล้ว (ไม่มีใน DB):** `sellable`, `unavailable_reason`, `price_discount`, `in_stock`, `quantity`

ถ้าไม่พบสินค้า: คืน `id`, `visible: false` + ฟิลด์อื่นเป็น `null`

chat-v2 คำนวณราคาสุทธิเองจาก `price`, `discount`, `discount_type` (เช่น `BAHT` / `PERCENTAGE`)

---

## 4) Webhook

```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` มี URL — chat-v2 ต้องถอดเป็น text ด้วย AI ตอน ingest (ดูหัวข้อ **Chat-v2: barcode / QR → text** ด้านบน)
---

## Env

```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
```

---

## ทดสอบ

```bash
php artisan test --filter=PharmacyInternalApiTest
```

---

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