# Schema & Mapping (เฟส 3)
อัปเดต: 20 กุมภาพันธ์ 2026

โฟกัสเฟส 3: ล็อกสัญญา field-level ระหว่าง Facebook Marketing API (Graph v19+) กับ canonical schema ที่ UI WepAppOpus ใช้

## Canonical schema (ยึดตามตาราง `ad_entries` ของ WepAppOpus)
- date, account_id, campaign_id, adset_id, ad_id
- campaign_name, ad_set_name, ad_name, objective, status
- spend, impressions, reach, clicks, link_clicks
- purchases, purchase_value, messaging, messaging_new_contacts
- video_3s, leads, results, page_engagement, post_engagement
- video_plays, thruplays
- gender, age_range, placement, device, country
- import_type, source_file, row_hash (dedup key)

## Mapping ตาราง API → Canonical

| Canonical | Graph field / rule | Type | Required | Default / Note |
|---|---|---|---|---|
| date | `date_start` (time_increment=1) | date | ✅ | format `YYYY-MM-DD` |
| account_id | `account_id` | string | ✅ | — |
| campaign_id | `campaign_id` | string | ✅ | — |
| adset_id | `adset_id` | string | ✅ | — |
| ad_id | `ad_id` | string | ✅ | — |
| campaign_name | `campaign_name` | string | ✅ | — |
| ad_set_name | `adset_name` | string | ✅ | — |
| ad_name | `ad_name` | string | ✅ | — |
| objective | `objective` | string | ▢ | passthrough |
| status | `effective_status` | string | ▢ | passthrough |
| spend | `spend` | number | ▢ | 0 if missing |
| impressions | `impressions` | int | ▢ | 0 |
| reach | `reach` | int | ▢ | 0 |
| clicks | `clicks` | int | ▢ | 0 |
| link_clicks | `link_clicks` | int | ▢ | 0 |
| purchases | `actions` filter `purchase` | int/float | ▢ | sum values; if value missing count actions |
| purchase_value | `action_values` filter `purchase` | number | ▢ | 0 |
| messaging | `actions` filter `onsite_conversion.messaging_conversation_started_7d` | number | ▢ | 0 |
| messaging_new_contacts | `actions` filter `onsite_conversion.messaging_first_reply` | number | ▢ | 0 (ต้องยืนยัน metric) |
| video_3s | `video_3_sec_watched_actions` | int | ▢ | 0 |
| leads | `actions` filter `lead` | int | ▢ | 0 |
| results | `results` | varies | ▢ | passthrough |
| page_engagement | `page_engagement` | int | ▢ | 0 |
| post_engagement | `post_engagement` | int | ▢ | 0 |
| video_plays | `video_play_actions` | int | ▢ | 0 (ยืนยัน metric variant) |
| thruplays | `video_thruplay_actions` | int | ▢ | 0 |
| gender | breakdown `gender` | string | ▢ | — |
| age_range | breakdown `age` | string | ▢ | — |
| placement | compose `publisher_platform` + `platform_position` | string | ▢ | fallback platform only |
| device | breakdown `device_platform` | string | ▢ | — |
| country | breakdown `country` | string | ▢ | — |
| import_type | constant `api` | string | ✅ | ใช้แยก pipeline |
| source_file | constant `api:{account_id}` | string | ✅ | ระบุที่มา |
| row_hash | hash(date, ad_id, age, gender, device_platform, publisher_platform, platform_position, country) | string | ✅ | dedup key |

## Null / Default policy
- ตัวเลขที่หาย: แปลงเป็น 0 เพื่อความสม่ำเสมอ (UI ปัจจุบันรองรับเลข)
- สตริงที่หาย: ตั้งเป็น `""` หรือเว้น `NULL` แล้วจัดการใน UI; ให้คงค่าชื่อ campaign/ad ไว้แม้ null
- Currency/timezone: ยึดตาม ad account (ไม่ convert)

## Breakdown & Hash
- ใช้ `time_increment=1` + breakdown `age,gender,device_platform,publisher_platform,platform_position,country`
- `row_hash` = stable hash ของ `date + ad_id + breakdown tuple` เพื่อ dedup/UPSERT

## ข้อมูลที่ pipeline จะเติมเอง
- import_id: สร้างตอน insert import_logs
- created_at: default DB

## จุดที่ยังต้องยืนยัน (ขึ้นกับเจ้าของบัญชี)
1) metric สำหรับ messaging_new_contacts (คาด `onsite_conversion.messaging_first_reply`)  
2) metric สำหรับ video_plays (เลือก `video_play_actions` หรือ variant `video_p25_watched_actions`)  
3) ช่วง backfill (แนะนำ 90 วัน) และ SLA การอัปเดต (T+1 หรือ near real-time)  
4) รายการ ad_account_id เป้าหมายและเพดาน rate limit ที่ยอมรับ  

## Artifacts ที่เพิ่มในเฟส 3
- โค้ด mapping: `src/mapping.ts`
- Unit test: `tests/mapping.test.ts` ตรวจ key หลัก/row_hash rule
