# Pharmacy Conversion Webhook — Laravel → chat-v2

> Laravel ยิง event กลับ chat-v2 เมื่อ funnel เกิดขึ้น  
> Chat เก็บใน `v2_conversion_events` แล้วแสดงบนหน้า Admin recommendations

**สถานะ Laravel:** ยิงทุก event type ด้านล่างเมื่อมี `ai_attribution` (`rec_id` + `chat_sid`)

---

## Endpoint

```http
POST {CHAT_V2_PUBLIC_URL}/api/internal/pharmacy/conversion-events
Authorization: Bearer {CHAT_WEBHOOK_SECRET}
```

หรือ:

```http
X-Webhook-Secret: {CHAT_WEBHOOK_SECRET}
```

ค่า secret ใช้ตัวเดียวกับ `CHAT_WEBHOOK_SECRET` / webhook embed อื่นใน monorepo

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

| สภาพแวดล้อม | URL |
|---|---|
| Local chat-v2 | `http://127.0.0.1:4563/api/internal/pharmacy/conversion-events` |
| Prod (ตัวอย่าง) | `https://the-medicine-chatbot-docker.nd.co.th/chat-v2/api/internal/pharmacy/conversion-events` |

### Env (Laravel)

```env
CHAT_V2_URL=http://127.0.0.1:4563
CHAT_WEBHOOK_SECRET=your-secret
# ออปชัน override URL โดยตรง:
# CHAT_CONVERSION_WEBHOOK_URL=http://127.0.0.1:4563/api/internal/pharmacy/conversion-events
```

ถ้าไม่ตั้ง `CHAT_CONVERSION_WEBHOOK_URL` → Laravel ใช้ `{CHAT_V2_URL}/api/internal/pharmacy/conversion-events` อัตโนมัติ

---

## Payload

```json
{
  "event": "paid",
  "occurred_at": "2026-07-21T14:30:00+07:00",
  "chat_sid": "conv-abc123",
  "rec_id": "550e8400-e29b-41d4-a716-446655440000",
  "product_id": 1320,
  "order_id": "SO-1001",
  "quantity": 2,
  "amount": 350.00,
  "member_uid": "member-uid-xxx"
}
```

### ฟิลด์

| ฟิลด์ | บังคับ | หมายเหตุ |
|---|---|---|
| `event` | ใช่ | ดูตาราง **Event types** |
| `occurred_at` | ใช่ | ISO 8601 |
| `chat_sid` | ใช่ | จาก `ai_attribution.chat_sid` |
| `rec_id` | ใช่ | จาก `ai_attribution.rec_id` |
| `product_id` | ไม่ | Laravel `product_products.id` (= `pid`) |
| `order_id` | ใช่สำหรับ cart/checkout/paid/refund/cancelled/payment_failed | `order_orders.no` (fallback `id`) |
| `quantity` | ไม่ | จำนวนชิ้นรวม |
| `amount` | ไม่ | ยอดเงิน THB (`grand_total`) |
| `member_uid` | ไม่ | member uid |

### Idempotency และการซื้อซ้ำ

- **Unique key:** `(event, order_id, rec_id)` — ออเดอร์เดิมส่งซ้ำ = update ไม่สร้างแถวใหม่ (ฝั่ง chat-v2)
- **rec_id เดียว + order_id ต่างกัน = ได้หลายแถว** — รองรับลูกค้ากลับมาซื้อซ้ำจากลิงก์เดิม (cookie `tm_ai_attr` 30 วัน)

---

## Event types

| `event` | ความหมาย | Trigger (Laravel) | สถานะ |
|---|---|---|---|
| `product_view_ai` | เปิด PDP จากลิงก์แชท | `FrontendController::product_list` + cookie/query | ✅ |
| `add_to_cart` | ใส่ตะกร้า | `OrderController::process_update_item` (`type=add`) | ✅ |
| `checkout_start` | เริ่มชำระเงิน | `OrderController::process_checkout` | ✅ |
| `paid` | ชำระสำเร็จ | `process_update_payment_status` → `COMPLETED` | ✅ |
| `cancelled` | ยกเลิกก่อนจ่าย | `OrderAdminController::set_status` → `CANCELLED` (ยังไม่ `COMPLETED`) | ✅ |
| `payment_failed` | ชำระไม่ผ่าน | `process_update_payment_status` → `FAILED` (ไม่เคย `COMPLETED`) | ✅ |
| `refund` | คืนเงิน | `OrderAdminController::set_status` → `CANCELLED` หลังจ่ายแล้ว | ✅ |

### กติกาสำคัญ

- **`paid` + `refund` แยก event** — ออเดอร์ที่จ่ายแล้วคืนเงิน = มีทั้งสองแถว
- **`cancelled`** — ยกเลิกก่อนจ่าย (หรือยกเลิกโดยไม่ผ่าน refund path)
- **`payment_failed`** — สลิปไม่ผ่าน / gateway fail — ต่างจาก `cancelled`
- ยิง `paid` เฉพาะเมื่อชำระสำเร็จจริง — ไม่ยิงตอนอัปโหลดสลิปที่ยัง `PROCESSING`
- ถ้า `ai_attribution` ว่าง → **ไม่ยิง**

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

```json
{ "event": "product_view_ai", "rec_id": "abc", "chat_sid": "conv-1", "product_id": 1320 }
{ "event": "add_to_cart", "rec_id": "abc", "order_id": "SO-1001" }
{ "event": "checkout_start", "rec_id": "abc", "order_id": "SO-1001" }
{ "event": "paid", "rec_id": "abc", "order_id": "SO-1001", "amount": 350 }
{ "event": "refund", "rec_id": "abc", "order_id": "SO-1001", "amount": 350 }
```

---

## โค้ด Laravel

| ไฟล์ | หน้าที่ |
|---|---|
| `Modules/Order/app/Services/ChatV2ConversionWebhookService.php` | สร้าง payload + dispatch |
| `Modules/Order/app/Jobs/SendChatV2ConversionWebhook.php` | HTTP POST (queue, retry 3 ครั้ง) |
| `Modules/Order/app/Http/Controllers/OrderController.php` | paid, payment_failed, add_to_cart, checkout_start |
| `Modules/Order/app/Http/Controllers/OrderAdminController.php` | cancelled, refund |
| `Modules/Frontend/app/Http/Controllers/FrontendController.php` | product_view_ai |
| `config/chat.php` | `conversion_webhook_url`, `webhook_secret` |

ทดสอบ: `tests/Feature/Order/SendChatV2ConversionWebhookTest.php`

---

## PDPA

ส่งได้: `order_id`, `amount`, `member_uid`, `chat_sid`, `rec_id`, `product_id`  
**ห้าม:** ที่อยู่, สลิป, บัตร, ข้อมูลสุขภาพ

---

## ฝั่ง Chat หลังรับ webhook

- เก็บ `v2_conversion_events` (ทุก event type)
- หน้า Admin `/admin/chat-v2/recommendations` แสดงสถานะรวม: ขายสำเร็จ / ยกเลิก / คืนเงิน / ชำระไม่ผ่าน / funnel
- % Recommend → Paid นับจาก `paid` เท่านั้น

---

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

- [`pharmacy-conversion-attribution-plan.md`](./pharmacy-conversion-attribution-plan.md)
- [`pharmacy-conversion-attribution-handoff.md`](./pharmacy-conversion-attribution-handoff.md)
