SDD-046 - Chương trình đối tác bán hàng
Yêu cầu: PRD-006. Đã hiện thực 27.09.2026 theo câu trả lời của chủ dự án (PRD-006 §0). Code: workers/api/src/modules/partners, apps/partners, apps/web/src/partnerTouch.ts, apps/admin/src/pages/Partners.tsx, migration 0304. Chỗ bản hiện thực khác bản nháp dưới đây: xem §7.
1. Quyết định chính
- Hệ riêng, không mở rộng referral learner. Bảng
partner_*riêng, module APIworkers/api/src/modules/partnersriêng, appapps/partnersriêng. Chỉ dùng chung: phiên đăng nhập (/v1/auth/google, cookie*.nemo12.com), bảng giápricing_items, sự kiện thanh toán. Lý do: luật của referral learner (R-25 cùng vai, tài khoản ảo) đang bảo vệ trẻ con và sổ tiêu nội bộ; nới chúng cho người bán là mở lỗ ở hệ đang chạy. - Một người mua, một nguồn (P-22). Hai câu đọc chéo: ghi công đối tác hỏi
referral_attributions, ghi công referral learner hỏipartner_attributions. Ai ghi trước thắng. - Sổ hoa hồng chỉ chèn như SDD-041 §2: số dư =
SUM, hoàn tiền = dòng âm. - Không lộ danh tính người mua (P-52): API bảng điều khiển trả
order_refbăm, ngày, sản phẩm, số tiền; không join sangusers/learners.
2. Kiến trúc
nemo12.com/ielts?p=ABC123&c=fb-thang10 (trang công khai, đã có)
│ lưu {p,c,ts} vào cookie .nemo12.com 30 ngày + localStorage (sống qua OAuth)
▼
POST /v1/partners/clicks (không cần phiên; đếm lượt bấm, rate-limit theo IP)
▼ đăng nhập Google lần đầu
POST /v1/auth/google ── hook attribution ──► partner_attributions (PK buyer_user_id)
▼ thanh toán được xác nhận (membership/SAT/Speaking)
event payment.confirmed ──► partner_commissions (+) , hold_until = paid_at + N ngày
event payment.refunded ──► partner_commissions (−)
▼
partners.nemo12.com (apps/partners: Vite + React + shadcn + Tailwind + motion)
admin.nemo12.com/partners (duyệt, trả tiền)3. Dữ liệu (migration 0304)
| Bảng | Việc | Ràng buộc giữ luật |
|---|---|---|
partners | Hồ sơ, mã, tầng trên, tài khoản ngân hàng | UNIQUE(user_id), UNIQUE(code), CHECK(upline <> id) |
partner_campaigns | Nhãn chiến dịch theo dòng sản phẩm | UNIQUE(partner_id, slug) |
partner_clicks | Đếm lượt bấm, không lưu IP | |
partner_attributions | Người mua thuộc đối tác nào | PRIMARY KEY(buyer_user_id): first touch trọn đời |
partner_sales | Khoản người mua thực trả | PRIMARY KEY(payment_ref): ghi hai lần không thành hai khoản |
partner_commissions | Sổ hoa hồng chỉ chèn; hoàn = dòng âm | UNIQUE(payment_ref, tier, kind), tỷ lệ chụp rate_bp |
partner_payouts | Yêu cầu rút, ngày trả, mã chuyển khoản | chỉ mục duy nhất một phần: một requested mỗi đối tác |
partner_rates | Tỷ lệ theo (dòng sản phẩm, tầng), basis point | seed 1000 / 200 |
Con số tiền (30 ngày, 5tr, 3tr, 1tr, ngày 5) ở workers/api/src/modules/partners/policy.ts.
4. API
| Route | Phiên | Việc |
|---|---|---|
POST /v1/partners/clicks | không | Đếm lượt bấm (rate-limit) |
POST /v1/partners/apply | user | Đăng ký, tự duyệt, nhận upline_code |
GET /v1/partners/me | user | Hồ sơ, chính sách, số dư, phễu, đơn ẩn danh, lịch sử rút |
POST /v1/partners/me/campaigns | đối tác | Tạo chiến dịch |
PUT /v1/partners/me/bank | đối tác | Khai ngân hàng |
POST /v1/partners/me/payouts, .../{id}/cancel | đối tác | Yêu cầu / huỷ rút |
GET /v1/admin/partners | admin | Đối tác + yêu cầu rút chờ trả |
POST /v1/admin/partners/{code}/status | admin | Khoá / mở khoá |
POST /v1/admin/partners/sales, .../sales/{ref}/refund | admin | Ghi khoản thu / hoàn |
POST /v1/admin/partners/payouts/{id}/paid | admin | Đánh dấu đã trả |
Ghi công nằm trong /v1/auth/google, ngay sau khi tạo phiên; hỏng thì chỉ log, không chặn đăng nhập.
5. Giao diện apps/partners
Từ SRC-1335 (chủ dự án 11.10.2026) khu đối tác dùng design system của www: nạp index.css + soft.css, theme-soft n12-v1 trên <body>, nền sáng, thanh trên sẫm n12-nav-deep, Card / Badge / Table / Label là bản sao canonical của packages/design-system/shadcn. Nền "biển sâu" cũ bỏ hẳn.
Sáu trang, mỗi trang một URL (src/router.tsx, SPA fallback ở wrangler.jsonc):
| URL | Trang | Nội dung |
|---|---|---|
/ | Tổng quan | Số dư (rút được ngay nổi bật nhất), lượt bấm / người mua / doanh số, bảng theo sản phẩm, ba lối tắt |
/links | Link | Link theo sản phẩm, link chiến dịch kèm số liệu, link mời đối tác |
/orders | Đơn hàng | Bảng khoản hoa hồng ẩn danh, nhãn trạng thái |
/payouts | Rút tiền | Số dư, gửi yêu cầu rút, tài khoản nhận tiền, lịch sử |
/kit | Tài liệu | Bài mẫu theo sản phẩm, gắn sẵn link |
/help | Hỏi đáp | Câu hỏi thường gặp, chính sách |
Link ?tab=overview|links|money|kit của bản một trang (thư đã gửi) được đổi sang URL mới bằng history.replaceState. Chưa đăng nhập → màn giới thiệu chương trình + nút Google. Đã đăng nhập mà chưa là đối tác → form đăng ký trong một thẻ.
6. Kiểm chứng
api-testtrên D1 thật: ghi công, một nguồn, tự bán, hoàn tiền, chạy lại sự kiện hai lần, quyền (partner A không đọc được của B; learner không vào được/me/stats).- e2e Playwright cho
apps/partners(375px + desktop).
7. Chỗ bản hiện thực khác bản nháp
- Không có bước duyệt:
partners.statuschỉ cònactive | suspended. - Không có bảng nguồn chung: luật một nguồn giữ bằng hai câu đọc chéo (§1.2). Đủ cho hai nguồn; nguồn thứ ba xuất hiện thì mới đáng một bảng chung.
- Ghi công bằng cookie
n12_partnertrên.nemo12.com, đọc ngay trong/v1/auth/google. localStorage không dùng được vì người mua bấm link ởnemo12.comnhưng đăng nhập ởlearn/marlins. - Tỷ lệ ở
partner_rates, không ởpricing_items: bảng sau được trang công khai đọc. - Khoản thu ở
partner_sales, do admin ghi (POST /v1/admin/partners/sales) vì chưa có cổng thanh toán; cổng về sau gọi cùngrecordSale(). - Rút tiền:
available = hoa hồng đã qua 30 ngày - (đang chờ trả + đã trả); rút tối đaavailable - 1.000.000khiavailable ≥ 5.000.000, mỗi lần ≥ 3.000.000; chỉ mục duy nhất một phần giữ mỗi đối tác một yêu cầurequested. Hoàn tiền sau khi đã rút làmavailableâm và trừ vào hoa hồng về sau.
8. Giảm giá người mua và thuế TNCN (migration 0305)
partner_attributions.buyer_discount_bpchụp 10% lúc ghi công.referral.summary().my_discounttrả luôn ưu đãi này khi người dùng không có ưu đãi referral, nên trang học phí learn và hai trang giới thiệu hiện nó mà không đổi giao diện. Admin traGET /v1/admin/partners/buyer?email=trước khi ghi khoản thu, và ghi số tiền SAU giảm.partner_payoutsthêmtax_rate_bp,tax_withheld_vnd,net_amount_vnd,tax_id, chụp lúc yêu cầu rút. Số rút là số gộp và trừ vào số dư đủ cả thuế.partners.tax_idbắt buộc trước khi rút.GET /v1/admin/partners/tax-report?year=tổng theo người cho tờ khai.
9. Mở lại và cô lập (10.10.2026, SRC-1332)
Ngày 27.09.2026 cả tính năng bị revert khỏi main (commit 8fa8544c) để nó không ảnh hưởng việc deploy tính năng khác. Revert chỉ gỡ phía API: app nemo12-partners đã deploy vẫn sống, nên partners.nemo12.com gọi /v1/partners/me và nhận 404 "Không tìm thấy tài nguyên". Bài học: tạm dừng một tính năng có giao diện riêng mà chỉ gỡ API là để lại một màn hỏng ngoài production.
Bản mở lại khôi phục nguyên PR #203 và thêm bốn chỗ:
| Chỗ | Việc |
|---|---|
Cờ PARTNERS_ENABLED | "0" thì mọi route /v1/partners/*, /v1/admin/partners* trả 503 "đang tạm dừng", và đăng nhập bỏ qua bước ghi công. Tạm dừng lần sau đổi một biến, không revert. Test: partners/routes.test.ts |
| Hai điểm chạm vào hệ đang chạy | Ghi công trong /v1/auth/google bọc catch, hỏng chỉ log; partnerTouch trên nemo12.com bọc try, gọi đếm lượt bấm không chờ. Cả hai không làm hỏng đăng nhập hay trang công khai |
| Link Speaking | Trỏ /speak (trang chương trình có từ sau 27.09). productOf() nhận cả /speak lẫn *-speaking* |
| Màn đối tác | Số liệu từng chiến dịch trên từng link, mục hỏi đáp (PRD-006 §7), lỗi tải có nút thử lại |
App partners deploy theo đường riêng trong .github/app-paths.json, nên một lỗi chỉ ở apps/partners không chặn app khác. Module API vẫn nằm trong worker chung: test của nó đỏ thì deploy API đỏ, như mọi module khác.
9a. Làm nốt cùng ngày
| Chỗ | Việc |
|---|---|
| Nhập mã tay | POST /v1/referral/claim: mã không phải của learner thì thử attributeBuyer() của đối tác, cùng luật (một nguồn, không tự bán, chỉ người mới), không có cửa sổ 30 ngày. Ô nhập ở thẻ "Your offers" trang /ielts/membership |
| Thư đối tác | email/campaignsPartners.ts chạy cùng cron 19:00 với thư giới thiệu, khoá riêng partner-mail. Bốn lá partner-welcome, partner-first-push, partner-sale, partner-paid; chống trùng bằng dedupeKey, cửa sổ 14 ngày, cờ PARTNERS_ENABLED="0" tắt cả lượt |
| Tài liệu | Trang "Tài liệu" (bài mẫu ở apps/partners/src/kitPosts.ts), từ SRC-1335 có URL riêng /kit |
| Admin | adminPartners() thêm clicks, clicks_30d; thẻ chỉ số kích hoạt trên trang Đối tác |