Skip to content

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 ​

  1. Hệ riêng, không mở rộng referral learner. Bảng partner_* riêng, module API workers/api/src/modules/partners riêng, app apps/partners riê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.
  2. 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ỏi partner_attributions. Ai ghi trước thắng.
  3. Sổ hoa hồng chỉ chèn như SDD-041 §2: số dư = SUM, hoàn tiền = dòng âm.
  4. Không lộ danh tính người mua (P-52): API bảng điều khiển trả order_ref băm, ngày, sản phẩm, số tiền; không join sang users/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ảngViệcRàng buộc giữ luật
partnersHồ sơ, mã, tầng trên, tài khoản ngân hàngUNIQUE(user_id), UNIQUE(code), CHECK(upline <> id)
partner_campaignsNhãn chiến dịch theo dòng sản phẩmUNIQUE(partner_id, slug)
partner_clicksĐếm lượt bấm, không lưu IP
partner_attributionsNgười mua thuộc đối tác nàoPRIMARY KEY(buyer_user_id): first touch trọn đời
partner_salesKhoản người mua thực trảPRIMARY KEY(payment_ref): ghi hai lần không thành hai khoản
partner_commissionsSổ hoa hồng chỉ chèn; hoàn = dòng âmUNIQUE(payment_ref, tier, kind), tỷ lệ chụp rate_bp
partner_payoutsYêu cầu rút, ngày trả, mã chuyển khoảnchỉ mục duy nhất một phần: một requested mỗi đối tác
partner_ratesTỷ lệ theo (dòng sản phẩm, tầng), basis pointseed 1000 / 200

Con số tiền (30 ngày, 5tr, 3tr, 1tr, ngày 5) ở workers/api/src/modules/partners/policy.ts.

4. API ​

RoutePhiênViệc
POST /v1/partners/clickskhôngĐếm lượt bấm (rate-limit)
POST /v1/partners/applyuserĐăng ký, tự duyệt, nhận upline_code
GET /v1/partners/meuserHồ sơ, chính sách, số dư, phễu, đơn ẩn danh, lịch sử rút
POST /v1/partners/me/campaignsđối tácTạo chiến dịch
PUT /v1/partners/me/bankđối tácKhai ngân hàng
POST /v1/partners/me/payouts, .../{id}/cancelđối tácYêu cầu / huỷ rút
GET /v1/admin/partnersadminĐối tác + yêu cầu rút chờ trả
POST /v1/admin/partners/{code}/statusadminKhoá / mở khoá
POST /v1/admin/partners/sales, .../sales/{ref}/refundadminGhi khoản thu / hoàn
POST /v1/admin/partners/payouts/{id}/paidadminĐá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):

URLTrangNội dung
/Tổng quanSố 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
/linksLinkLink theo sản phẩm, link chiến dịch kèm số liệu, link mời đối tác
/ordersĐơn hàngBảng khoản hoa hồng ẩn danh, nhãn trạng thái
/payoutsRút tiềnSố dư, gửi yêu cầu rút, tài khoản nhận tiền, lịch sử
/kitTài liệuBài mẫu theo sản phẩm, gắn sẵn link
/helpHỏi đápCâ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-test trê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.status chỉ còn active | 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_partner trên .nemo12.com, đọc ngay trong /v1/auth/google. localStorage không dùng được vì người mua bấm link ở nemo12.com như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ùng recordSale().
  • Rút tiền: available = hoa hồng đã qua 30 ngày - (đang chờ trả + đã trả); rút tối đa available - 1.000.000 khi available ≥ 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ầu requested. Hoàn tiền sau khi đã rút làm available â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_bp chụp 10% lúc ghi công. referral.summary().my_discount trả 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 tra GET /v1/admin/partners/buyer?email= trước khi ghi khoản thu, và ghi số tiền SAU giảm.
  • partner_payouts thêm tax_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_id bắ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ạyGhi 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 SpeakingTrỏ /speak (trang chương trình có từ sau 27.09). productOf() nhận cả /speak lẫn *-speaking*
Màn đối tácSố 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ã tayPOST /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ácemail/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ệuTrang "Tài liệu" (bài mẫu ở apps/partners/src/kitPosts.ts), từ SRC-1335 có URL riêng /kit
AdminadminPartners() thêm clicks, clicks_30d; thẻ chỉ số kích hoạt trên trang Đối tác