SMP Business Rules¶
Audience: BA, PM, Dev, QC, Ops · Updated: v3.3
Mục đích¶
Tài liệu này tập trung mọi business rule rải rác trong SPEC v3.x. Khi dev code, QC test, ops vận hành — đây là single source of truth cho logic.
Mỗi rule có ID BR-<category>-<num> để reference từ user story, test case, code comment.
🔮 Migration plan v4.0 · Markdown rules → rules_engine.yaml¶
Quan trọng: Format hiện tại (Markdown prose) phù hợp cho v3.x đọc/review thủ công. Khi sang v4.0, toàn bộ rules sẽ migrate sang YAML + load runtime qua
antonmedv/expr(xem Doc 05 · Glossary section O).
Tại sao migrate?¶
| Vấn đề hiện tại (v3.x Markdown) | Giải pháp v4.0 (YAML + expr) |
|---|---|
| Thay đổi rule = code change + deploy | Update ConfigMap → hot-reload trong 30s |
Rule khác nhau theo country = nhiều if trong Go code |
YAML có when: country == 'VN' |
| Khó test rule độc lập | Unit test YAML rule với mock context |
| BA muốn xem rule phải đọc Go code | BA đọc YAML, hiểu ngay |
| Khó audit "rule X có hiệu lực từ khi nào" | YAML version trong Git, blame thấy ai sửa khi nào |
Rule ID convention mới · v4.0¶
country_code: ISO 3166-1 alpha-2 lowercase (vn,us,cn,th,id,sg)category:dispatch,pricing,payment,kyc,stage,material,partner,integration,quality,notification,retentionnum: 3 digit zero-padded (001,025,100)
Examples:
- vn.dispatch.001 — Round timeout Vietnam (60s)
- us.dispatch.001 — Round timeout US (90s, do thợ rải rác hơn)
- vn.pricing.007 — VAT 10% Vietnam
- sg.pricing.007 — GST 9% Singapore
- *.kyc.001 — Áp dụng tất cả country (wildcard)
Mapping với BR-* cũ: rule BR-DISP-001 v3.x → vn.dispatch.001 v4.0.
Format comparison · 1 rule trong 2 format¶
Format hiện tại (v3.x Markdown):
### BR-DISP-001 · Round timeout
Mỗi dispatch round có timeout **60 giây** (production), **30 giây** (dev/staging)
để thợ accept. Nếu không có ai accept → tự động chuyển sang round tiếp theo.
Format mới (v4.0 YAML):
- id: vn.dispatch.001
name: "Round timeout Vietnam"
category: dispatch
description: "Mỗi dispatch round có timeout 60s production, 30s dev/staging."
enabled: true
priority: 100
when: "context.country_code == 'VN'"
then:
round_timeout_seconds: 60
overrides:
- when: "context.env in ['dev', 'staging']"
then:
round_timeout_seconds: 30
changelog:
- { version: "3.0", date: "2026-01-15", author: "BA team", change: "Initial" }
Migration roadmap¶
| Phase | Khi nào | Action |
|---|---|---|
| Phase 0 | v3.4 (now) | Giữ Markdown. Doc này là source. |
| Phase 1 | v3.5 (Q3 2026) | Build tool: parse Markdown → generate YAML draft. Review thủ công. Deploy ConfigMap parallel với code rules cũ. |
| Phase 2 | v3.6 (Q3 2026) | Switch code over: dispatch-engine + finance-svc + pricing logic đọc từ YAML. Markdown thành reference. |
| Phase 3 | v4.0 (Q4 2026) | YAML = source of truth. Markdown auto-generate từ YAML cho human-readable. |
Cấu trúc file rules_engine.yaml (preview cho v4.0)¶
version: "4.0"
last_updated: "2026-11-01"
maintainers: ["ba-team@smp.vn"]
# Context schema · các biến rule có thể reference
context_schema:
country_code: string # 'VN', 'US', 'CN'
env: string # 'dev', 'staging', 'prod'
order: object # full order entity
agent: object # full agent entity
customer: object # full customer entity
partner: object | null # nếu order qua partner
now: timestamp # UTC timestamp
rules:
# === DISPATCH ===
- id: vn.dispatch.001
name: "Round timeout"
# ... (như trên)
- id: vn.dispatch.002
name: "Max rounds"
when: "context.country_code == 'VN'"
then:
max_rounds: 3
# === PRICING ===
- id: vn.pricing.001
name: "Labor price multiplier by level"
when: "context.country_code == 'VN'"
then:
multiplier_map:
junior: 0.8
mid: 1.0
senior: 1.3
- id: vn.pricing.007
name: "VAT Vietnam"
when: "context.country_code == 'VN'"
then:
vat_rate: 0.10
- id: sg.pricing.007
name: "GST Singapore"
when: "context.country_code == 'SG'"
then:
vat_rate: 0.09
# ... 80+ rules
Đọc rules trong code Go (v4.0 preview)¶
import "github.com/expr-lang/expr"
type RulesEngine struct {
rules []Rule
cache map[string]*vm.Program // compiled expressions
}
func (e *RulesEngine) Evaluate(category string, ctx Context) map[string]any {
var matched []Rule
for _, r := range e.rules {
if r.Category != category || !r.Enabled {
continue
}
program := e.cache[r.ID]
result, err := expr.Run(program, ctx)
if err == nil && result.(bool) {
matched = append(matched, r)
}
}
// sort by priority, merge `then` results
return mergeThens(matched)
}
// Usage trong dispatch-engine:
ctx := Context{CountryCode: "VN", Env: "prod", Order: order}
result := engine.Evaluate("dispatch", ctx)
timeout := result["round_timeout_seconds"].(int) // 60
A · Dispatch Rules¶
BR-DISP-001 · Round timeout¶
Mỗi dispatch round có timeout 60 giây (production), 30 giây (dev/staging) để thợ accept. Nếu không có ai accept → tự động chuyển sang round tiếp theo.
BR-DISP-002 · Max rounds¶
Tối đa 3 rounds (production), sau đó đơn được escalate sang ops dispatch tay.
BR-DISP-003 · Round expansion¶
- Round 1: invite tất cả agent qualified online trong cùng
districtcủa customer - Round 2: mở rộng sang các district lân cận (within 5km hoặc cùng
coverage_zone) - Round 3: invite mọi agent qualified trong
city, kể cả offline (push notification)
BR-DISP-004 · Qualified agent definition¶
Agent qualified để nhận đơn khi:
- agents.status = 'active'
- agents.is_online = true (round 1-2)
- Có agent_skills matching service step's skill_id với level >= step.min_level
- KYC level ≥ basic
- Không bị suspended trong tháng
BR-DISP-005 · Private dispatch logic (v3.3)¶
Nếu orders.source = 'partner_customer' AND dispatch_visibility = 'private':
- Tất cả rounds chỉ dispatch cho agents thuộc partner_id của đơn
- Nếu 3 rounds fail → escalate to ops (NOT mở rộng cho thợ partner khác)
BR-DISP-006 · Open dispatch with partner preference (v3.3)¶
Nếu dispatch_visibility = 'open' AND partner_id != NULL:
- Round 1: dispatch cho agents của partner trước
- Round 2-3: mở rộng cho tất cả agents qualified
BR-DISP-007 · Surge multiplier¶
Khi coverage_zone.surge_multiplier > 1.0:
- labor_price × multiplier applied to all steps in zone đó
- Hiển thị notice cho customer trước khi đặt: "Khu vực này đang phụ phí giờ cao điểm +XX%"
- Multiplier tính lúc create order, không đổi khi đơn đang chạy
BR-DISP-008 · Manual dispatch override¶
Ops admin với scope dispatch.manage có thể assign đơn cho bất kỳ agent active, không cần qualified theo skill — nhưng phải có lý do (audit log).
BR-DISP-009 · Concurrent invitation cap¶
1 agent không nhận quá 2 invitation đồng thời. Nếu agent đang xử lý 2 đơn → bị exclude khỏi round.
B · Pricing Rules¶
BR-PRICE-001 · Labor price by level¶
labor_price cho 1 step = steps.labor_price_l<N> với N = level của agent assigned.
Nếu chưa biết level (lúc estimate trước khi assign): dùng labor_price_l3 (mặc định).
BR-PRICE-002 · Material sell price¶
material_variants.sell_price do SMP set, không lấy từ wms cost. SMP control margin.
BR-PRICE-003 · Quote subtotal¶
labor_subtotal = SUM(order_steps.labor_price)
material_subtotal = SUM(order_step_materials.unit_price * quantity)
survey_fee = service.survey_fee (default 80,000đ)
discount = applied voucher amount (max voucher.max_discount)
vat = (labor + material + survey - discount) * 10%
total = labor + material + survey - discount + vat
BR-PRICE-004 · Survey fee policy¶
- Customer trả survey fee TRƯỚC khi Survey Agent xuất phát (default 80,000đ)
- Survey fee:
- Hoàn 100% nếu Survey Agent không đến trong cam kết (no-show)
- Hoàn 50% nếu customer từ chối báo giá (compensate cho thợ đã đi)
- Không hoàn nếu khách duyệt báo giá (đã được trừ vào tổng đơn)
BR-PRICE-005 · Voucher discount¶
- Customer nhập voucher_code → SMP gọi
inside.vouchers.validate(code, customer_id, amount) - inside trả về
discount_amount(có thể là % hoặc fixed) - SMP áp dụng discount tối đa
voucher.max_discount - Voucher chỉ áp dụng 1 lần per đơn
BR-PRICE-006 · Surge applies to labor only¶
Surge multiplier áp dụng cho labor_price, không áp dụng cho material_price.
BR-PRICE-007 · Partner pricing override (v3.3)¶
Nếu partner có custom pricing config:
- partners.customer_config.labor_discount_percent: giảm % labor cho partner đó
- partners.customer_config.no_material_markup: nếu true, partner trả material theo cost_price (không markup)
Default: không override.
BR-PRICE-008 · VAT¶
- VAT 10% áp dụng cho mọi đơn cho cá nhân
- Đơn B2B với partner_type = 'business': VAT 10% với invoice xuất tên công ty partner
- Đơn
source = 'contract': VAT theo terms hợp đồng (có thể 8% hoặc khác)
BR-PRICE-009 · Rounding¶
- Tất cả calculations giữ precision integer (VND no decimal)
- Discount/percent rounding: ROUND HALF UP (vd 12,505 → 12,510 nếu round to 10)
C · Payment Rules¶
BR-PAY-001 · Payment timing¶
- Customer direct: thanh toán sau khi duyệt báo giá (stage 06)
- Partner customer (prepaid wallet): trừ wallet ngay khi tạo đơn (stage 01)
- Partner customer (monthly invoice): ghi nợ vào invoice, không trừ ngay
BR-PAY-002 · Refund logic¶
| Cancel at stage | Refund |
|---|---|
| 01-02 (chưa có thợ) | 100% nếu đã trả |
| 03-04 (thợ đang đến) | 100% trừ phí khảo sát |
| 05 (đã khảo sát, chờ duyệt) | 100% trừ 50% phí khảo sát (40k giữ lại) |
| 06-07 (đã duyệt, chờ thực hiện) | 100% labor + 100% material (nếu wms cho cancel reservation) |
| 08 (đang sửa) | Pro-rata: trả labor đã làm + material đã dùng, refund phần còn lại |
| 09-10 (đã xong) | Không refund (trừ trường hợp dispute) |
BR-PAY-003 · Partner wallet topup¶
- Topup methods: bank_transfer, e-wallet
- Bank transfer: tạo wallet_transaction status = 'pending', confirm sau khi tài chính SMP verify (manual)
- E-wallet: confirm tự động qua webhook
- Minimum topup: 1,000,000đ
- Maximum topup per transaction: 100,000,000đ (above cần manual approval)
BR-PAY-004 · Partner wallet debit¶
Khi tạo đơn partner_customer:
1. Pre-check wallet_balance >= total_amount
2. Atomic: lock wallet row + insert wallet_transaction (type='order_debit') + update balance
3. Nếu order cancelled: insert wallet_transaction (type='refund') hoàn lại
BR-PAY-005 · Insufficient wallet¶
- API trả 402
insufficient_balancevới detail - UI hiển thị "Cần nạp thêm
đ" - Không allow tạo đơn nếu thiếu
D · KYC Rules¶
BR-KYC-001 · Agent KYC levels¶
| Level | Required docs | Allowed actions |
|---|---|---|
pending |
None | View pool, can't accept |
basic |
CCCD front+back, portrait selfie, bank statement | Accept orders, max value 5M đ/đơn |
full |
basic + skill certificate + insurance | All orders, no value cap |
BR-KYC-002 · KYC progression¶
- Agent đăng ký → pending
- Upload docs basic → review → approve/reject (manual ops)
- Nếu approve → status = active, level = basic
- Sau 10 đơn completed + rating ≥ 4.0 → có thể upgrade to full (request)
- Upgrade to full requires submit thêm certificate + insurance
BR-KYC-003 · Partner KYC levels (v3.3)¶
| Level | Required docs | Allowed actions |
|---|---|---|
pending |
None | View only, can't create orders |
basic |
GPKD/CCCD rep, STK ngân hàng, bằng nghề (1 thợ chính) | Tạo đơn, max 50 đơn/tháng |
full |
basic + hợp đồng dịch vụ + bảo hiểm trách nhiệm | Tạo đơn không giới hạn, supplier mode |
BR-KYC-004 · KYC verification responsibility¶
- Agent KYC: SMP ops verify (theo doc image quality + cross-check info)
- Partner KYC: SMP ops Partner Squad verify, có thể cần outsource thẩm định business
BR-KYC-005 · Rejection process¶
- Reject → status không đổi, gửi notification với reason
- User có thể re-upload (max 3 lần)
- Sau 3 lần fail → require manual call/meeting
E · Order Stage Transition Rules¶
BR-STAGE-001 · Allowed transitions¶
Chỉ các transitions sau là valid (state machine):
01_created → 02_dispatched_survey (auto, sau khi payment OK)
02_dispatched_survey → 03_survey_accepted (khi agent tap "Nhận")
02_dispatched_survey → 02_dispatched_survey (round 2, 3)
02_dispatched_survey → needs_manual_dispatch (sau 3 rounds fail)
03_survey_accepted → 04_arrived_survey (khi agent check-in GPS gần customer)
04_arrived_survey → 05_surveyed (agent submit báo giá)
05_surveyed → 06_quote_approved (customer duyệt)
05_surveyed → cancelled (customer từ chối)
06_quote_approved → 07_dispatched_execution (auto, nếu khác agent than survey)
06_quote_approved → 08_in_progress (auto, nếu cùng agent — Dual)
07_dispatched_execution → 08_in_progress (khi execution agent accept)
08_in_progress → 09_completed (khi agent submit completion)
09_completed → 10_rated (khi customer rate)
9_completed → 10_rated (auto sau 24h nếu không rate)
Any stage → cancelled (with reason, ngoại trừ 09, 10)
BR-STAGE-002 · Cancel restrictions¶
- Stage 08 (đang sửa): chỉ ops admin cancel được, không phải customer/agent
- Stage 09, 10: KHÔNG cancel được — phải qua dispute flow
BR-STAGE-003 · GPS check-in¶
Stage transit 03 → 04 yêu cầu agent GPS within 300m của customer address. Nếu xa hơn → require justification + ops review.
BR-STAGE-004 · Photo proof required¶
Stages requiring photo: - 04: 1 ảnh thiết bị "before" - 05: tổng ảnh ≥ 3 (before, during, các phần hư hỏng) - 08: ảnh "after" per step (mỗi step ít nhất 1) - 09: 1 ảnh "final" tổng quan
Không upload đủ → không submit được transition.
BR-STAGE-005 · Stage 10 rating logic¶
- Customer rate 5: agent earnings × 1.05 bonus (5%)
- Customer rate 4: agent earnings × 1.00 standard
- Customer rate 3: agent earnings × 0.95
- Customer rate 1-2: agent earnings × 0.90 + auto-trigger dispute
BR-STAGE-006 · Auto-cancel timers¶
- Stage 01: nếu không pay trong 30 phút → auto cancel
- Stage 05: nếu customer không duyệt báo giá trong 48h → auto cancel + refund 50% phí khảo sát
F · Material Rules¶
BR-MAT-001 · Stock check timing¶
- Trong quote (stage 05): real-time check wms
- Khi customer duyệt (stage 06): reserve trong wms (TTL 24h)
- Khi step start (stage 08): không cần check lại (đã reserved)
- Khi step done (stage 08 từng step): commit out trong wms (trừ stock thật)
BR-MAT-002 · Reservation TTL¶
- Default 24h
- Auto-release nếu order cancelled hoặc stage không tiến trong 24h
- Có thể extend bởi ops admin (nếu khách báo lùi lịch)
BR-MAT-003 · Free-form material¶
- Allowed khi agent không tìm được variant phù hợp trong catalog
- Required fields: name, quantity, unit, unit_price
- Tự động
verify_status = 'pending_verify' - Đơn vẫn complete được, nhưng task verify hiện cho ops
- Ops 3 lựa chọn: Approve off-catalog | Reject + agent compensate | Request clarification
BR-MAT-004 · BOM variance detection¶
Sau khi step done, compare actual vs expected BOM: - Variance ≤ 5%: auto-verified - Variance 5-15%: flag for review (informational) - Variance > 15%: blocked until ops approve
BR-MAT-005 · Material from personal warehouse¶
Agent có thể chỉ định dùng material từ kho cá nhân (warehouse_type = 'personal'): - Stock check: chỉ check warehouse cá nhân - Commit: trừ warehouse cá nhân, không trừ kho chung - Agent earnings: get full material margin (vì agent đã invest vào kho)
G · Partner Platform Rules (v3.3)¶
BR-PTN-001 · Partner types¶
business: có GPKD, MST, rep_name (đại diện pháp luật) — invoice xuất tên công tyindividual_large: cá nhân lớn, không GPKD — invoice xuất tên cá nhân
BR-PTN-002 · Partner roles¶
1 partner có thể có 1 hoặc cả 2 roles:
- customer: đặt đơn (Type A)
- supplier: cung cấp đội thợ (Type B)
- Cả hai (Type AB): vd Hùng AC Service có đội thợ + còn đặt đơn outsource khi quá tải
BR-PTN-003 · Supplier models¶
Partner supplier có 3 model trả phí:
| Model | SaaS fee | Commission | Thợ ăn |
|---|---|---|---|
commission_only |
0 | 6% | 94% |
saas_only |
5M/tháng | 0 | 100% |
hybrid |
2M/tháng | 3% | 97% |
BR-PTN-004 · Payment modes (customer-side)¶
prepaid_wallet: nạp ví trước, trừ khi tạo đơnmonthly_invoice: ghi nợ, NET 30 (default), xuất hoá đơn cuối tháng
BR-PTN-005 · Payout modes (supplier-side)¶
passthrough: SMP trả tiền công trực tiếp vào TK thợvia_partner: SMP trả tổng vào TK partner, partner tự trả thợ off-platform
BR-PTN-006 · Partner user roles (RBAC)¶
| Role | Số người tối đa | Permissions |
|---|---|---|
partner_owner |
1 (founder/CEO) | All within partner |
partner_manager |
Unlimited | Operations (orders, agents) |
partner_finance |
Unlimited | Finance only (wallet, invoices, reports) |
partner_dispatcher |
Unlimited | Operations + dispatch toggle |
Owner có thể demote/promote khác, nhưng không thể demote chính mình (cần SMP ops help).
BR-PTN-007 · Wallet alert thresholds¶
- Balance < 7 ngày trung bình spending → alert email + in-app
- Balance < 3 ngày → block tạo đơn mới
- Negative balance: KHÔNG cho phép (atomic check)
BR-PTN-008 · Partner-specific dispatch¶
Nếu partner có ≥ 5 active agents AND customer_config.default_dispatch_visibility = 'private' → default đặt đơn là private dispatch.
H · Integration Rules (v3.2)¶
BR-INT-001 · Customer source of truth¶
insidelà source of truth cho customer profile + payment methods + vouchers- SMP không store customer name/phone permanent, chỉ cache 30s
- Snapshot per-order: name + address tại thời điểm tạo (lưu trong smp_order.orders)
BR-INT-002 · Webhook signature¶
Mọi inbound webhook phải có HMAC SHA-256 signature trong header X-Signature:
Outbound webhook (SMP → partner): tương tự, secret từ partner.webhook_secret.
BR-INT-003 · Webhook retry¶
- Outbound: exponential backoff 1s, 10s, 60s, 5min, 30min
- Sau 5 lần fail → DLQ + alert ops
- Manual replay từ admin UI
BR-INT-004 · Idempotency¶
- Mọi POST endpoint hỗ trợ
Idempotency-Keyheader - Server cache response 24h theo key
- Same key + same body → return cached response (idempotent)
- Same key + different body → 409 conflict
BR-INT-005 · Circuit breaker¶
Per dependency (inside, wms): - Trip after 5 consecutive failures OR error rate > 50% over 1 min window - Half-open after 30s: try 1 request - Close if success, else remain open
I · Quality Rules¶
BR-QUAL-001 · Auto-dispute trigger¶
Auto-create dispute task khi: - Customer rate ≤ 2 sao - Customer comment chứa keywords: "lừa", "không xong", "tệ" (config trong settings) - Photo proof < 50% required count - Stage 08 duration > 3× expected step duration
BR-QUAL-002 · Dispute SLA¶
- Ops respond first contact: 4h
- Resolution target: 48h
- Escalate to manager: > 48h
- Customer satisfaction survey: sent 24h sau resolution
BR-QUAL-003 · Agent suspension triggers¶
Auto-suspend nếu trong 30 ngày: - avg_rating < 3.0 (min 5 đơn) - no_show ≥ 3 lần - 2 disputes resolved against agent - Photo proof fraud detected (lặp ảnh, ảnh không khớp)
Suspend = status = 'suspended', không thể nhận đơn cho đến khi ops review unsuspend.
BR-QUAL-004 · Partner suspension triggers¶
Auto-suspend partner nếu: - Outstanding invoice overdue > 60 ngày - Multiple compliance violations (KYC docs giả, fraud reports)
J · Notification Rules¶
BR-NOTIF-001 · Customer notification channels¶
| Event | Push | SMS | |
|---|---|---|---|
| Order created | ✓ | ||
| Agent assigned | ✓ | ||
| Agent on the way | ✓ | ||
| Quote ready | ✓ | ✓ | |
| Order completed | ✓ | ✓ (with invoice) | |
| Refund processed | ✓ | ✓ |
BR-NOTIF-002 · Agent notification channels¶
| Event | Push | SMS |
|---|---|---|
| New order invitation | ✓ | |
| Order assigned | ✓ | |
| Customer message | ✓ | |
| Earnings paid | ✓ | ✓ |
BR-NOTIF-003 · Partner notification channels¶
| Event | Push | |
|---|---|---|
| Order completed | ✓ (daily digest, not per order) | |
| Wallet low | ✓ | ✓ |
| Invoice generated | ✓ | |
| KYC status change | ✓ | |
| Agent KYC pending review | ✓ |
BR-NOTIF-004 · Quiet hours¶
- Customer: không gửi push 22:00 - 07:00 (trừ urgent: agent on the way, payment confirmation)
- Agent: không gửi push 22:00 - 06:00 (trừ scheduled order < 8h tới)
BR-NOTIF-005 · Frequency cap¶
- Customer: max 3 push/day (chưa kể order updates)
- Agent: no cap on order invitations (revenue critical)
K · Data Retention Rules¶
BR-RET-001 · Order data¶
- Active + completed orders: keep indefinitely in MySQL (until contractual obligation expires)
- Order > 5 năm: archive to S3 (read-only), drop from MySQL
- Final delete only after 7 năm + legal review
BR-RET-002 · Audit log¶
- Hot storage 90 ngày
- Warm storage 2 năm
- Cold (S3 Glacier) 7 năm
- Financial audit entries: 10 năm (tax compliance)
BR-RET-003 · Photos¶
- Photo proof: 2 năm post-completion, then archive
- Customer-uploaded: customer can delete via account settings (deletion + 30d grace)
- KYC photos: 5 năm post-termination
BR-RET-004 · Event log¶
- TTL 90 ngày (MongoDB TTL index)
- Đủ cho debug + recent analytics
BR-RET-005 · Session/tokens¶
- Access token: 8h
- Refresh token: 30 ngày
- Session in Redis: TTL = token expiry
L · Reconciliation & Fraud Detection Rules (v4.0)¶
Bộ rules cho automated reconciliation (đối soát) và fraud detection. Daily jobs check inconsistency + flag suspicious patterns.
BR-RECON-001 · Daily wallet reconciliation¶
- Schedule: Daily 02:00 UTC (sau khi end-of-day payment gateway closes)
- Source A: SMP
partner_wallet_transactionscumulative balance per partner - Source B: Payment gateway transaction export (VNPay/MoMo/ZaloPay) per partner_ref
- Acceptable diff: < 0.01% OR < 100k VND, whichever lower
- Action if exceed: Alert Finance + create reconciliation ticket
- Owner: Finance team reviews mỗi sáng
BR-RECON-002 · Daily KYC status reconciliation¶
- Schedule: Daily 03:00 UTC
- Check: Partner/Agent với KYC docs uploaded > 7 ngày nhưng status vẫn
pending_kyc - Action: Page Ops Admin queue · max review time 48h SLA
- Escalation: nếu > 14 ngày, escalate Operations Manager
BR-RECON-003 · Daily order-payment reconciliation¶
- Schedule: Daily 02:30 UTC
- Check:
- Orders
status=completednhưngpayment_status=pending> 24h → flag - Payments
status=succeedednhưng không có order linked → flag - Refunds requested nhưng không có payment gateway entry → flag
- Threshold alert: > 10 outstanding items
- Owner: Finance + Engineering joint review
BR-RECON-004 · Monthly currency conversion audit (v4.0)¶
- Schedule: First day of month, 04:00 UTC
- Check: Every multi-currency transaction trong tháng trước có rate snapshot đúng (rate ngày txn vs rate stored)
- Tolerance: < 0.001% diff (currency conversion precision)
- Output: Monthly report email to Finance + CFO
BR-FRAUD-001 · Wallet topup velocity check¶
- Pattern: > 5 topups trong 10 phút từ cùng partner
- Severity: Warning
- Action: Soft-flag, require email verification on 6th topup. No block.
BR-FRAUD-002 · Geographic anomaly¶
- Pattern: Wallet topup từ IP country khác với partner registered country
- Severity: High
- Action: Block transaction · require manual review (Finance + Security)
- Exception: Partner mark "travel" flag trước 24h (Marketing white-list)
BR-FRAUD-003 · Unusual amount¶
- Pattern: Single topup > 10x partner's running average + first time at this scale
- Severity: High
- Action: Hold txn for manual review (max 2h business hours)
BR-FRAUD-004 · AML threshold (Anti-Money Laundering)¶
- Pattern (VN): Single txn ≥ 300,000,000 VND OR cumulative ≥ 500,000,000 VND/day from 1 partner
- Action: Hold + report to SBV (State Bank of Vietnam) per Decree 03/2022/NĐ-CP
- Audit log: Required permanent record
- Owner: Compliance team handles SBV filing
BR-FRAUD-005 · Multi-account device fingerprint¶
- Pattern: Same device fingerprint creates > 3 customer/agent accounts trong 24h
- Severity: Medium
- Action: Flag accounts for review · could be legitimate (shop owner) or fraud ring
BR-FRAUD-006 · Refund abuse¶
- Pattern: Customer/Partner requests > 5 refunds trong tháng OR refund rate > 30%
- Severity: Medium
- Action: Review pattern, possible legitimate complaints vs fraud ring
- Threshold for action: > 8 refunds/month → temporary suspend pending investigation
BR-RECON-005 · Cross-region data consistency (v4.0)¶
- Schedule: Every 6 hours
- Check: Master data (countries, currencies, currency_rates, tax_configs) consistent across all clusters (smp-asia, smp-china, smp-us)
- Tolerance: ZERO drift cho master data
- Action nếu drift: Auto-trigger MirrorMaker re-sync + alert DevOps
- Owner: DevOps + Data Engineering
BR-RECON-006 · Audit log integrity check¶
- Schedule: Daily 04:30 UTC
- Check: Audit log có gap (missing audit_id sequence) hoặc hash chain broken (xem Doc 12 · Audit Log section 10 cho hash chain spec)
- Severity: SEV-1 (potential tamper)
- Action: Immediate page Security team + CTO
Quy trình thay đổi business rule¶
- Đề xuất change → tạo proposal doc (impact: customers, partners, agents, finance)
- PM + Founder approve
- Dev đánh giá technical effort
- QC viết test cases cho new rule
- Implement + deploy to staging
- UAT với select users
- Communicate to affected stakeholders 7 ngày trước GA
- Deploy prod + monitor
- Update doc này + relevant other docs
Quan trọng: Rule nào active trong production → MUST đồng bộ với doc này. Nếu khác → bug hoặc doc outdated, phải fix.
Appendix · Full rules_engine.yaml sample (v4.0 preview)¶
Phần này là preview cho v4.0 rules engine. Convert sample chỉ cho 2 categories Dispatch + Pricing. Các categories còn lại (Payment, KYC, Stage, Material, Partner, Integration, Quality, Notification, Retention) sẽ convert tương tự khi v3.6.
Cấu trúc file đầy đủ¶
# rules_engine.yaml
# Single source of truth cho 80+ business rules
# Deploy: kubectl apply -f rules-configmap.yaml
# Audience: Edit by BA team, review by Tech Lead, audited via Git
version: "4.0.0"
last_updated: "2026-11-01T08:00:00Z"
maintainers:
- "ba-team@smp.vn"
- "tech-lead@smp.vn"
# ===========================================================================
# Context schema · Biến available trong expressions
# ===========================================================================
context_schema:
country_code: string # 'VN', 'US', 'CN', 'TH', 'ID', 'SG', 'MY', 'PH'
env: string # 'dev', 'staging', 'prod'
now: timestamp # UTC timestamp
order:
id: string
source: string # 'customer_direct', 'partner_customer', 'contract'
partner_id: string|null
current_stage: string
country_code: string
currency: string
subtotal_amount: int64 # minor units
total_amount: int64
agent:
id: int64
level: string # 'junior', 'mid', 'senior'
rating: float
completed_orders: int
skills: array<string>
home_district: string
home_city: string
is_online: bool
status: string
customer:
id: int64
is_vip: bool
completed_orders: int
partner:
id: int64|null
type: string|null # 'A', 'B', 'AB'
payment_mode: string|null # 'prepaid_wallet', 'post_invoice'
wallet_balance: int64|null
payout_mode: string|null # 'direct', 'via_partner'
# ===========================================================================
# A · DISPATCH RULES
# ===========================================================================
rules:
- id: vn.dispatch.001
name: "Round timeout · Vietnam"
category: dispatch
description: "Mỗi dispatch round có timeout 60s production, 30s dev/staging."
enabled: true
priority: 100
when: "context.country_code == 'VN'"
then:
round_timeout_seconds: 60
overrides:
- when: "context.env in ['dev', 'staging']"
then:
round_timeout_seconds: 30
legacy_id: "BR-DISP-001"
changelog:
- { version: "3.0", date: "2026-01-15", author: "BA team", change: "Initial" }
- id: us.dispatch.001
name: "Round timeout · US"
category: dispatch
description: "US thợ rải rác, cần 90s. Test nội bộ Q3 2026."
enabled: false # not yet launched
priority: 100
when: "context.country_code == 'US'"
then:
round_timeout_seconds: 90
- id: vn.dispatch.002
name: "Max rounds"
category: dispatch
enabled: true
priority: 100
when: "context.country_code == 'VN'"
then:
max_rounds: 3
escalate_action: "ops_manual_dispatch"
legacy_id: "BR-DISP-002"
- id: vn.dispatch.003
name: "Round radius expansion"
category: dispatch
description: "Round 1: same district. Round 2: +5km. Round 3: full city."
enabled: true
priority: 100
when: "context.country_code == 'VN'"
then:
radius_per_round:
- { round: 1, scope: "same_district" }
- { round: 2, scope: "within_km", value: 5 }
- { round: 3, scope: "same_city", include_offline: true }
legacy_id: "BR-DISP-003"
- id: "*.dispatch.004"
name: "Qualified agent definition"
category: dispatch
description: "Áp dụng tất cả country. Filter agents qualified."
enabled: true
priority: 100
when: "true" # always
then:
qualified_filters:
- "agent.status == 'active'"
- "agent.is_online == true"
- "agent.rating >= 3.5"
- "agent.kyc_level in ['basic', 'advanced', 'premium']"
legacy_id: "BR-DISP-004"
- id: vn.dispatch.005
name: "Private dispatch · partner-only agents"
category: dispatch
description: "Order partner-private chỉ gửi thợ thuộc partner đó."
enabled: true
priority: 200 # higher priority, overrides default
when: "context.order.dispatch_visibility == 'private' && context.order.partner_id != null"
then:
filter_extra: "agent.partner_id == context.order.partner_id"
fallback_action: "escalate_to_ops" # nếu không có agent partner online
legacy_id: "BR-DISP-005"
- id: vn.dispatch.007
name: "Surge multiplier per round"
category: dispatch
description: "Round 1 = 1.0x, round 2 = 1.2x, round 3 = 1.5x. Surge chỉ áp dụng labor."
enabled: true
priority: 100
when: "context.country_code == 'VN'"
then:
surge_multipliers:
round_1: 1.00
round_2: 1.20
round_3: 1.50
applies_to: ["labor"] # not material
legacy_id: "BR-DISP-007"
# ===========================================================================
# B · PRICING RULES
# ===========================================================================
- id: vn.pricing.001
name: "Labor price multiplier by agent level"
category: pricing
description: "Junior 0.8x, Mid 1.0x, Senior 1.3x base price."
enabled: true
priority: 100
when: "context.country_code == 'VN'"
then:
level_multipliers:
junior: 0.8
mid: 1.0
senior: 1.3
legacy_id: "BR-PRICE-001"
- id: vn.pricing.002
name: "Material sell price"
category: pricing
description: "Sell price từ catalog. SMP controlled, không tự ý discount."
enabled: true
priority: 100
when: "context.country_code == 'VN'"
then:
source: "catalog.material_variants.sell_price_amount"
legacy_id: "BR-PRICE-002"
- id: vn.pricing.003
name: "Quote subtotal calculation"
category: pricing
enabled: true
priority: 100
when: "context.country_code == 'VN'"
then:
formula: "labor_total + material_total + survey_fee - discount"
legacy_id: "BR-PRICE-003"
- id: vn.pricing.005
name: "Voucher discount · pre-VAT"
category: pricing
description: "Voucher trừ vào subtotal trước khi tính VAT."
enabled: true
priority: 100
when: "context.country_code == 'VN' && context.order.voucher_code != null"
then:
apply_order: ["subtotal", "discount", "vat", "total"]
discount_cap_percent: 50 # max 50% subtotal
legacy_id: "BR-PRICE-005"
- id: vn.pricing.006
name: "Surge applies to labor only"
category: pricing
description: "Multiplier surge chỉ nhân vào labor, không nhân material/fee."
enabled: true
priority: 100
when: "context.country_code == 'VN'"
then:
surge_target: "labor"
legacy_id: "BR-PRICE-006"
- id: vn.pricing.007
name: "VAT Vietnam"
category: pricing
enabled: true
priority: 100
when: "context.country_code == 'VN'"
then:
vat_rate: 0.10
vat_name: "VAT"
lookup_table: "tax_configs" # actual rate fetched runtime để support changes
legacy_id: "BR-PRICE-008"
- id: sg.pricing.007
name: "GST Singapore"
category: pricing
enabled: false # not yet launched
priority: 100
when: "context.country_code == 'SG'"
then:
vat_rate: 0.09
vat_name: "GST"
- id: us.pricing.007
name: "Sales Tax US · jurisdiction-dependent"
category: pricing
enabled: false
priority: 100
when: "context.country_code == 'US'"
then:
vat_rate_source: "tax_configs" # query by state/county/city
vat_name: "Sales Tax"
- id: vn.pricing.009
name: "Rounding rules · Vietnam"
category: pricing
description: "VND không decimal. Round to nearest 1000 VND cho display."
enabled: true
priority: 100
when: "context.country_code == 'VN'"
then:
storage_minor_units: 1 # 1đ = 1 minor unit (no decimal)
display_rounding: 1000 # round 47,523 → 47,500
display_rounding_mode: "down"
legacy_id: "BR-PRICE-009"
- id: us.pricing.009
name: "Rounding rules · US"
category: pricing
enabled: false
priority: 100
when: "context.country_code == 'US'"
then:
storage_minor_units: 100 # 1 USD = 100 cents
display_rounding: 1 # cent precision
display_rounding_mode: "nearest"
- id: vn.pricing.010
name: "Partner pricing override"
category: pricing
description: "Partner Type B có thể override labor price (contract negotiated)."
enabled: true
priority: 200
when: |
context.partner != null &&
context.partner.type in ['B', 'AB'] &&
context.partner.has_pricing_override
then:
use_partner_pricing: true
source: "partner.pricing_config"
legacy_id: "BR-PRICE-007"
Đọc + apply rules trong code Go¶
// dispatch-engine/internal/dispatch/round.go
package dispatch
import (
"context"
"smp/pkg/rules"
)
type RoundController struct {
engine *rules.Engine
log *zap.Logger
}
func (r *RoundController) StartRound(ctx context.Context, order Order) error {
// Build evaluation context
ruleCtx := rules.Context{
"country_code": order.CountryCode,
"env": os.Getenv("ENV"),
"now": time.Now().UTC(),
"order": toRuleMap(order),
}
// Eval dispatch rules
decision := r.engine.Evaluate("dispatch", ruleCtx)
timeout := decision.GetInt("round_timeout_seconds", 60) // default 60
maxRounds := decision.GetInt("max_rounds", 3)
// Use values từ rules engine
timer := time.NewTimer(time.Duration(timeout) * time.Second)
// ...
return nil
}
Testing rules¶
# tests/rules/dispatch_test.yaml
test_cases:
- name: "VN production · 60s timeout"
context:
country_code: "VN"
env: "prod"
expect:
round_timeout_seconds: 60
max_rounds: 3
- name: "VN dev · 30s timeout (override)"
context:
country_code: "VN"
env: "dev"
expect:
round_timeout_seconds: 30
- name: "Private dispatch with partner agents"
context:
country_code: "VN"
env: "prod"
order:
dispatch_visibility: "private"
partner_id: "P001"
expect:
filter_extra: "agent.partner_id == 'P001'"
Run với: