Diagram 1 · Pipeline Overview
ภาพรวม Pipeline 7 ขั้นตอน
ค่าคอมมิชชั่นไม่ได้เกิดในขั้นตอนเดียว แต่ไหลผ่าน 7 stage เรียงตามเวลา — กดที่การ์ดแต่ละใบเพื่อดูรายละเอียด field/ตารางที่เกี่ยวข้อง (ออเดอร์ตัวอย่าง SO001 ฿10,000 เดียวกับ "เดินทางของเงิน") (source §1)
1 ยืนยัน Sale Order (Confirm) action_confirm() → FREEZE ว่าใครได้เครดิต + สายงาน (genealogy) ณ วินาทีนั้น ▾
sale_order: ผู้ได้เครดิต (ulife_commission_partner_id), สายงานแช่แข็ง (genealogy_path_snapshot), sponsor ณ เวลานั้น, และวันที่งวด ดูรายละเอียดเต็มที่ Diagram 22 คำนวณ QV/CV compute อัตโนมัติจากตาราง ulife_product_qvcv (matrix: สินค้า × ประเภทสมาชิก) ▾
res_partner + เก็บ audit ราย snapshot ที่ commission_qv_cv_daily — ตัวเลขพวกนี้ ใช้แสดงผล/ตรวจคุณสมบัติเท่านั้น ไม่ใช่ฐานเงินจริง ดูรายละเอียดที่ Diagram 33 ส่งของครบ (delivery trigger) delivery_status = 'full' ← จุดชนวนเงิน (กฎข้อ 2) ▾
ulife.delivery.attempt ส่งสำเร็จแบบ DSL/messenger (3) สมาชิกกดยืนยันรับของเอง หรือระบบประมาณจากพัสดุค้าง 14 วัน — บางส่วนจึงเป็น "วันส่งของแบบประมาณการ" ไม่ใช่ยืนยันจากขนส่งเสมอไป4 ค่าคอม 10% รายวัน (FRONT) cron 02:00 ทุกคืน — 1 แถวใน commission_line ต่อ 1 ออเดอร์ (base = cv_total) ▾
5 รอบเดือน (title + bonus) cron 3 ตัว ลำดับ 10 → 20 → 30 — ห้ามสลับ ▾
6 ปิดงวด + ปล่อยจ่าย + หัก WHT Manager กด action_close() → cron ปล่อย pending → available → หัก WHT ตรงนี้ ▾
7 ถอนเงิน (Withdrawal) + โอนผ่าน BBL portal → account_payment → BBL SFTP → MT940 ยืนยัน → state = 'paid' → ใบ 50 ทวิ ▾
Centerpiece · Interactive · Animated
การเดินทางของเงิน — ตัวอย่างจริงทีละสเต็ป (Order CV ฿10,000)
ตามเหรียญทีละบาทจาก 1 ออเดอร์ ว่าไปลงตารางไหนบ้าง — กด ▶ เล่นต่อเนื่อง หรือกดจุดเพื่อกระโดดไปดูสเต็ปไหนก็ได้
sale_order
commission_line — คุณเอ
commission_line — คุณบี
commission_nid_ytd
account_move
account_payment
สำหรับทีม IT · Interactive · Animated
มุมมองฐานข้อมูล (Technical) — ลงลึกระดับตาราง
ออเดอร์เดิม ฿10,000 (SO001) จาก "การเดินทางของเงิน" ด้านบน แต่คราวนี้ดูเป็น row จริงในตาราง ทีละ INSERT/UPDATE — เขียวกะพริบ = แถวใหม่ · เหลืองกะพริบ = cell ที่ถูกแก้ (ตัวอย่างสมมติ — ตัวเลขตรงกับส่วน "เดินทางของเงิน" ด้านบนทุกจุด)
sale_order
| name | partner | qv_total | cv_total | ulife_commission_partner_id | genealogy_path_snapshot | sponsor_snapshot_id | ulife_commission_date | commission_eligible | delivery_status |
|---|
commission_line
| id | partner | sale_order_id | commission_type | period_yymm | base_amount | rate | amount | wht_amount | net_amount | state | available_date | payment_id |
|---|
commission_line_source
| id | commission_line_id | sale_order_id | source_partner_id | cv_amount | period_yymm |
|---|
commission_nid_ytd
| nid | year | gross |
|---|
ulife_commission_period
| name | date_start | date_end | state | closed_by | closed_at |
|---|
account_move
| id | date | dr_account | dr_amount | cr_account | cr_amount | ref |
|---|
account_payment
| id | partner | payment_type | amount | is_wht_applied | state | settlement_move_id |
|---|
sale_order_id / commission_line_id — นี่คือ audit-immutability ที่กล่าวถึงใน Diagram 3 และ 5Diagram 2 · STAGE 1
แช่แข็ง (freeze) ตอน Confirm — ระบบล็อกอะไรบ้าง
เมื่อกด Confirm ระบบเขียนชุด "attribution snapshot" ลง sale_order แล้วล็อกไว้ถาวร — ต่อให้ภายหลังสมาชิกย้ายสาย/เปลี่ยน sponsor ค่าเหล่านี้ก็ไม่เปลี่ยนตาม (source §2)
partner_id ตรงๆaction_confirm() ไม่แตะ qv_total/cv_total และไม่สร้าง commission.line ใดๆ ทั้งสิ้น — ตรงกับกฎข้อ 2🛒 กรณี "ซื้อให้ทีมงาน" (Buy-for) — จุดที่เข้าใจผิดบ่อย
affiliate ซื้อของแล้วยกเครดิตให้ downline (ตั้ง ulife_credit_partner_id) — กติกาแยกเป็น 2 มิติที่ไม่ตรงกัน:
ขนาดของยอด (magnitude)
QV/CV คิดจาก member_type ของผู้ซื้อ — เพราะราคา/ภาษี/VAT ถูกคิดที่ระดับผู้ซื้อ
การระบุเจ้าของ (attribution)
เงินไหลขึ้นสายของ ulife_credit_partner_id (ผู้ได้เครดิต) — ไม่ใช่สายของผู้ซื้อ
Diagram 3 · STAGE 2
QV/CV มาจากไหน เก็บที่ไหน
ต้นทางคือตาราง matrix ulife_product_qvcv — 1 แถวเก็บว่า "สินค้า X ขายให้สมาชิกประเภท Y ได้ QV/CV เท่าไร" (source §3.1)
| สินค้า (ตัวอย่างประกอบ) | retail | shopper | wellness | affiliate | employee | vip |
|---|---|---|---|---|---|---|
| สินค้า A | QV 80 / CV 800 | QV 100 / CV 1,000 | — | — | — | QV 100 / CV 1,200 |
| สินค้า B | — ✗ ไม่มี fallback | QV 60 / CV 600 | QV 60 / CV 600 | QV 60 / CV 600 | — | — |
| Auto-ship line | ไม่ใช้ matrix — ใช้ ulife_program_qv/cv ที่ stamp มาบนบรรทัดโปรแกรมแทน | |||||
| บรรทัดของแถม | ulife_is_freebie = True → QV/CV = 0 เสมอ | |||||
สัดส่วนที่จ่ายจริง (paid-ratio proration)
ถ้าออเดอร์มีส่วนลด/voucher จะเฉลี่ย QV/CV ลงตามสัดส่วนเงินที่จ่ายจริง:
ผลลัพธ์ไปลงที่ไหน
qv_total / cv_total
~01:00 ทุกคืน
current_month_qv/cv, qv/cv_ytd
🏷️ display only
res_partner และ commission_qv_cv_daily เป็นข้อมูลไว้แสดงผล/ตรวจคุณสมบัติเท่านั้น — เงินจริงตอนคิดโบนัสรอบเดือนจะคำนวณใหม่จากออเดอร์ต้นทางเสมอ (audit-immutability) compute อ้างอิง partner_id.member_type = ประเภทของ "ผู้ซื้อ" เสมอ (ไม่ใช่ผู้ได้เครดิต)Diagram 4 · STAGE 4
ใครได้ 10% — การหาผู้รับ (payee walk-up)
เดินขึ้นจาก earner (ผู้ได้เครดิตที่ freeze ไว้) ตามสายงานแช่แข็ง จนเจอโหนดแรกที่ exists + active + member_type='affiliate' + earns_commission=True (source §4.2, method _front_commission_payee)
เดินสายจาก earner ขึ้นไปหา payee
ตัวอย่างประกอบ: earner ไม่ใช่ affiliate → upline ชั้นแรกถูกระงับ (ข้าม) → upline ชั้นถัดไปคือ payee
🧮 ตัวอย่างค่าคอม Front
Interactive · Animated
การย้ายสายงาน — Node Switch (รหัสสมาชิกเดิม แต่ย้ายไปเกาะ upline ใหม่)
สิ่งที่คนสับสนบ่อยที่สุด: "ย้ายสาย" ไม่ได้แปลว่าเปลี่ยนรหัสสมาชิกหรือสร้างสมาชิกใหม่ — สิ่งที่ย้ายคือ "โหนด" ในผังสายงาน
รหัสไม่ย้าย — โหนดย้าย
ulife_genealogy_node
1 สมาชิกมีได้หลายแถวตามประวัติการย้าย แต่ active ได้ทีละแถว
ulife_genealogy_node_event
audit log แบบ append-only ของทุกการย้าย
A → B → A — ทีละสเต็ป (ตัวอย่างจริงจาก test suite)
อ้างอิงจาก test_a_b_a_within_one_period_splits_volume_and_reactivates_node + test_shopper_switch_creates_then_reactivates — ทุกออเดอร์ CV 1,000
A — upline เดิม
B — upline ใหม่
S — คุณ S (Shopper)
node record rail
ออเดอร์ (CV 1,000 ต่อออเดอร์)
audit log — ulife_genealogy_node_event
🔩 กฎเหล็ก 4 ข้อ
รหัสสมาชิกไม่เคยเปลี่ยน — สิ่งที่ย้ายคือ "โหนด" ในผังสายงาน (ulife_genealogy_node) ไม่ใช่ตัวสมาชิกใน res_partner
Shopper ย้ายได้เสมอ ไม่มี freeze window (ตรงมติ PO 7 ก.ค.) — กรอกรหัส sponsor ใหม่ตอน checkout แล้วกด "ยืนยันคำสั่งซื้อ" · Wellness/Affiliate = ล็อกสาย ต้องให้ Back Office ย้ายให้เท่านั้น
ออเดอร์เก่าไม่ย้ายตาม — ทุกออเดอร์ freeze สาย ณ ตอนยืนยันตลอดไป สิ่งที่เปลี่ยนคือออเดอร์อนาคตเท่านั้น
A→B→A ไม่สร้างประวัติซ้ำ — reactivate โหนดเดิม (กันด้วย SQL unique) + audit log ครบทุกการย้าย
เกร็ดสำคัญ
Diagram 5 · commission_line
สถานะของค่าคอม (state machine)
pending → available → paid เป็นเส้นทางหลัก มีแขนงไป held และ reversed / clawback (source §6.2, §6.3, §4.4)
Front vs Monthly — เงื่อนไข release ต่างกัน
Front: เป็นแค่นาฬิกา settlement window — available_date ≤ today พอ ไม่ผูกกับ invoice/payment
Monthly: ต้องรอ Period._is_closed() = True ด้วย (PERIOD_GATED_RELEASE_TYPES) — ถ้ายังไม่ปิดงวด จะค้างไว้และ self-heal หลังปิด
งวดปิดแล้ว = ห้ามแก้ย้อนหลัง
แถวสถานะ paid/reversed ไม่เคยถูก mutate ต้องใช้ commission.adjustment (audit immutable, Manager-gated) หรือ clawback ในงวดปัจจุบันที่ยังเปิดเท่านั้น
Diagram 6 · STAGE 5
รอบเดือน — 3 cron เรียงลำดับ 10 → 20 → 30
ลำดับนี้ load-bearing — partner-tier อ่านผล Paid-As จาก title-eval; team-bonus อ่านตำแหน่งที่ resolve แล้ว รันขนานกันไม่ได้ (source §5.2)
_cron_monthly_title_eval
- คำนวณ PGV (L0) หน่วย QV + คู่หน่วย CV โดย key ตาม affiliate ancestor ที่สดใกล้ที่สุดบนสาย frozen
- เขียน paid_as = ตำแหน่งสูงสุดที่ pgv_l0_min ≤ pgv (ratchet up เท่านั้น)
- Bonus-Plus: gate + จ่าย บน CV → สร้าง commission_line ชนิด bonus_plus
_cron_monthly_partner_tier
- Dual gate: T0(PTV) ≥ 100,000 และ L0(PGV) ≥ 30,000
- เลือกตำแหน่งหมวด partner สูงสุดที่ผ่านเกณฑ์ legs/ttv → เขียน active_partner_legs, ttv, title
- Pass 2: สร้าง commission_line ชนิด partner_t1..t6 ต่อ generation ที่มี CV
_cron_monthly_team_bonus
- team_l0 (ฐาน PGV-CV หรือ own_cv ตาม config) + team_l1/l2/l3 (ฐาน CV ต่อ generation)
- rate: team_l1/l2/l3 = 3% / 3% / 3%
- ตัด generation ด้วยเกณฑ์ 10K tree-cut (ดูด้านล่าง)
การตัด generation — 10K tree-cut
โหนดที่มียอด ≥ 10,000 (config ulife_commission.team_cut_threshold_thb) จะ "ปิด generation" ของตัวเอง (รวมยอดตัวเอง, subtree เปิด generation ถัดไป) โหนด < 10,000 ยอดไหลขึ้นและเดินลึกต่อ
commission.title ณ เวลารัน cron ไม่มี FK ดังนั้นแก้อัตราภายหลังไม่ย้อนหลัง มีผลเฉพาะงวดอนาคตDiagram 7 · STAGE 7
ถอนเงิน (Withdrawal) → โอนผ่าน BBL
Swimlane: ■ สมาชิก · ■ ระบบ (Odoo) · ■ ธนาคาร (BBL) · ■ บัญชี/GL (source §7.1–7.4)
POST /api/v1/portal/withdraw
อ่านจาก commission_line: state='available', available_date≤today, payment_id=False, เก่าสุดก่อน
reserve lines (greedy oldest-first)
_reserve_lines_for_amount() — แถว clawback (ติดลบ) ถูกดึงมารวมก่อนเสมอ
สร้าง account.payment (draft)
_create_bbl_payout_payment() — outbound, amount=net, is_wht_applied=True → เขียน commission_line.payment_id ทันที (ปิดหน้าต่างจ่ายซ้ำ)
🏢 ประตูนิติบุคคล (juristic gate)
is_company=True → wht_method='pnd53' → state='awaiting_documents' (post ไม่ได้) → ต้องมีเลขใบกำกับภาษี (ulife_tax_invoice_number) → Commission-Manager กด action_ulife_juristic_approve() → approved → route "odoo_internal" (ไม่ผ่าน BBL rail) · WHT = PND53 flat 3%
bulk approve → account.batch.payment
bbl_payout_kind = affiliate_daily / member_monthly → สร้างไฟล์ HDR/DTL/TRL (res_partner_bank.bbl_proxy_type = bankac/natid/msisdn)
push ผ่าน ulife.bbl.sftp.push (Phase 1, live)
log ที่ ulife.bbl.sftp.log · Phase-2 BBL Direct REST ยังปิดอยู่ (config OFF)
ธนาคารยืนยันกลับด้วยไฟล์ MT940
wizard ulife.bbl.mt940.import → _mt940_set_paid_cascade()
payment.state = 'paid' → cascade
commission_line ที่ผูกอยู่ → state='paid', เขียน paid_date
_book_bbl_settlement_move() — idempotent
DR 210203 Commission Payable (gross) CR bank (net) CR 210401 WHT Payable PND1 CR 210501 WHT Payable PND53
_cron_autowithdraw_foreign กวาดเข้า batch AUTOWITHDRAW บน rail ประเทศนั้นโดยอัตโนมัติ — ไม่ผ่านหน้า portal ถอนเงินแบบเดียวกับในประเทศDiagram 8 · STAGE 6-7
WHT ก้าวหน้า (PND1) — หักเฉพาะส่วนที่เพิ่มขึ้น
หักตอน release (pending→available) ไม่ใช่ตอนถอน · ค่าลดหย่อน ฿169,000 + เพดานช่วง 0% แรก ฿150,000 = จุดเริ่มเสียภาษีที่ ฿319,000 ของ gross YTD (source §8.2, §8.4)
commission_wht_bracketcommission_nid_ytd (1 แถวต่อ NID+ปี) เพราะภาษีเงินได้ผูกกับตัวบุคคล นิติบุคคลที่สลับผู้ดำเนินการกลางปี → คนใหม่เริ่มจากฐาน 0 ของตัวเอง ไม่รับช่วงฐานของคนเก่านิติบุคคล — PND53 flat 3%
is_company=True → wht_method='pnd53' → wht_amount = gross × 3% เสมอ ไม่ใช้บันไดก้าวหน้า
Clawback ไม่คืน WHT
แถว clawback carry wht_amount=0 แต่ดึง gross เดิมออกจาก YTD ledger — WHT ที่หักไปแล้วไม่เคยถูก "คืน" เป็นบรรทัด
Diagram 9 · ภาคผนวก ก
ตารางฐานข้อมูลทั้งหมด — จัดกลุ่มตาม stage
17 ตารางที่ pipeline แตะ พร้อมสถานะข้อมูล — ดู ERD ฉบับเต็ม 4 หน้าที่ footer
STAGE 1-2 Sale Order & QV/CV
STAGE 4-6 หัวใจค่าคอม
STAGE 6-7 ภาษี + เงินได้สะสม
STAGE 7 จ่ายเงิน / ธนาคาร
ภาคผนวก ข (เลือก 10 ข้อที่เกี่ยวข้องกับธุรกิจมากที่สุด)
เข้าใจผิด ❌ vs ที่จริง ✅
รวมข้อควรระวังที่พบบ่อยที่สุดจากทั้งหมด 17 ข้อในเอกสารต้นฉบับ