@codebakers/mcp 5.6.1 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +22 -3
- package/dist/index.js.map +1 -1
- package/dist/tools/get-context.js +3 -3
- package/dist/tools/get-context.js.map +1 -1
- package/dist/tools/map-dependencies.js +21 -0
- package/dist/tools/map-dependencies.js.map +1 -1
- package/dist/tools/map-mlm-dependencies.d.ts +19 -0
- package/dist/tools/map-mlm-dependencies.d.ts.map +1 -0
- package/dist/tools/map-mlm-dependencies.js +451 -0
- package/dist/tools/map-mlm-dependencies.js.map +1 -0
- package/dist/tools/run-interview.d.ts +4 -2
- package/dist/tools/run-interview.d.ts.map +1 -1
- package/dist/tools/run-interview.js +13 -11
- package/dist/tools/run-interview.js.map +1 -1
- package/dist/tools/start.js +6 -6
- package/package.json +2 -1
- package/templates/BUILD-STATE.md +76 -0
- package/templates/BUSINESS-RULES-TEMPLATE.md +750 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
# Business Rules Documentation
|
|
2
|
+
|
|
3
|
+
**Project:** [Your Project Name]
|
|
4
|
+
**Domain:** [MLM / E-commerce / CRM / etc.]
|
|
5
|
+
**Last Updated:** [Date]
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
This file documents business logic that cannot be automatically inferred from schema or mockups.
|
|
12
|
+
|
|
13
|
+
Use this to capture:
|
|
14
|
+
- Computed field formulas
|
|
15
|
+
- Conditional triggers
|
|
16
|
+
- Cascade chains
|
|
17
|
+
- Business rule enforcement
|
|
18
|
+
- Complex calculations
|
|
19
|
+
|
|
20
|
+
**This complements:**
|
|
21
|
+
- `DEPENDENCY-MAP.md` (FK and read dependencies from codebakers_map_dependencies)
|
|
22
|
+
- `MLM-DEPENDENCIES.md` (Upline cascades from codebakers_map_mlm_dependencies)
|
|
23
|
+
|
|
24
|
+
Together, these provide **100% dependency coverage**.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Computed Fields
|
|
29
|
+
|
|
30
|
+
### [Entity].[field_name]
|
|
31
|
+
|
|
32
|
+
**Formula:**
|
|
33
|
+
```sql
|
|
34
|
+
[SQL or pseudocode formula]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Computed From:**
|
|
38
|
+
- [List source tables/fields]
|
|
39
|
+
|
|
40
|
+
**Update Triggers:**
|
|
41
|
+
- [Action that triggers recalculation]
|
|
42
|
+
- [Another trigger]
|
|
43
|
+
|
|
44
|
+
**Cascades To:**
|
|
45
|
+
- [What happens after this field updates]
|
|
46
|
+
|
|
47
|
+
**Affected Stores:**
|
|
48
|
+
- [StoreNames]
|
|
49
|
+
|
|
50
|
+
**Affected Components:**
|
|
51
|
+
- [ComponentNames]
|
|
52
|
+
|
|
53
|
+
**Example:**
|
|
54
|
+
```javascript
|
|
55
|
+
// When this field changes:
|
|
56
|
+
if ([condition]) {
|
|
57
|
+
[action]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
**Template Example:**
|
|
64
|
+
|
|
65
|
+
### user.personal_volume
|
|
66
|
+
|
|
67
|
+
**Formula:**
|
|
68
|
+
```sql
|
|
69
|
+
SUM(sales.bonus_volume WHERE sales.user_id = user.id AND sales.status = 'completed')
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Computed From:**
|
|
73
|
+
- sales table (bonus_volume column)
|
|
74
|
+
- Filtered by user_id and status
|
|
75
|
+
|
|
76
|
+
**Update Triggers:**
|
|
77
|
+
- createSale (when status = 'completed')
|
|
78
|
+
- updateSale (when status changes to/from 'completed')
|
|
79
|
+
- deleteSale
|
|
80
|
+
|
|
81
|
+
**Cascades To:**
|
|
82
|
+
- rank_check (if personal_volume >= next threshold)
|
|
83
|
+
- team_volume_recalc (for upline users)
|
|
84
|
+
- commission_calculation
|
|
85
|
+
|
|
86
|
+
**Affected Stores:**
|
|
87
|
+
- UserStore (current user)
|
|
88
|
+
- UserStore (all upline - for team_volume recalc)
|
|
89
|
+
|
|
90
|
+
**Affected Components:**
|
|
91
|
+
- User-Dashboard (shows personal_volume)
|
|
92
|
+
- Team-View (used in team calculations)
|
|
93
|
+
|
|
94
|
+
**Example:**
|
|
95
|
+
```javascript
|
|
96
|
+
// After createSale:
|
|
97
|
+
user.personal_volume += sale.bonus_volume
|
|
98
|
+
|
|
99
|
+
// Then check:
|
|
100
|
+
if (user.personal_volume >= RANK_THRESHOLDS[nextRank]) {
|
|
101
|
+
promoteUser(user.id, nextRank)
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Conditional Triggers
|
|
108
|
+
|
|
109
|
+
### [trigger_name]
|
|
110
|
+
|
|
111
|
+
**Condition:**
|
|
112
|
+
```
|
|
113
|
+
[When this happens]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Check:**
|
|
117
|
+
```javascript
|
|
118
|
+
if ([condition]) {
|
|
119
|
+
[action]
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Cascades To:**
|
|
124
|
+
- [What this triggers]
|
|
125
|
+
|
|
126
|
+
**Affected Entities:**
|
|
127
|
+
- [Entities that get updated]
|
|
128
|
+
|
|
129
|
+
**Example Implementation:**
|
|
130
|
+
```typescript
|
|
131
|
+
[Code example]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
**Template Example:**
|
|
137
|
+
|
|
138
|
+
### rank_promotion_check
|
|
139
|
+
|
|
140
|
+
**Condition:**
|
|
141
|
+
```
|
|
142
|
+
When personal_volume OR team_volume changes
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Check:**
|
|
146
|
+
```javascript
|
|
147
|
+
if (user.personal_volume >= RANK_THRESHOLDS[nextRank] ||
|
|
148
|
+
user.team_volume >= TEAM_RANK_THRESHOLDS[nextRank]) {
|
|
149
|
+
promoteUser(user.id, nextRank)
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Cascades To:**
|
|
154
|
+
- Create rank_promotion record
|
|
155
|
+
- Update user.rank
|
|
156
|
+
- Recalculate commission_rate (based on new rank)
|
|
157
|
+
- Trigger upline_bonus_recalc (sponsor's leadership bonus may change)
|
|
158
|
+
- Send promotion notification
|
|
159
|
+
|
|
160
|
+
**Affected Entities:**
|
|
161
|
+
- user (rank updated)
|
|
162
|
+
- rank_promotions (new record created)
|
|
163
|
+
- commissions (rate changes for future commissions)
|
|
164
|
+
- upline users (leadership bonuses recalculated)
|
|
165
|
+
|
|
166
|
+
**Example Implementation:**
|
|
167
|
+
```typescript
|
|
168
|
+
async function checkRankPromotion(userId: string) {
|
|
169
|
+
const user = await getUser(userId)
|
|
170
|
+
const currentRank = user.rank
|
|
171
|
+
const nextRank = getNextRank(currentRank)
|
|
172
|
+
|
|
173
|
+
const threshold = RANK_THRESHOLDS[nextRank]
|
|
174
|
+
const teamThreshold = TEAM_RANK_THRESHOLDS[nextRank]
|
|
175
|
+
|
|
176
|
+
if (user.personal_volume >= threshold || user.team_volume >= teamThreshold) {
|
|
177
|
+
// Promote
|
|
178
|
+
await updateUser(userId, { rank: nextRank })
|
|
179
|
+
await createRankPromotion({ user_id: userId, old_rank: currentRank, new_rank: nextRank })
|
|
180
|
+
|
|
181
|
+
// Cascade: Update upline leadership bonuses
|
|
182
|
+
await recalculateUplineLeadershipBonuses(user.sponsor_id)
|
|
183
|
+
|
|
184
|
+
// Cascade: Send notification
|
|
185
|
+
await sendPromotionNotification(userId, nextRank)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Cascade Chains
|
|
193
|
+
|
|
194
|
+
### [cascade_name]
|
|
195
|
+
|
|
196
|
+
**Trigger:** [Initial action]
|
|
197
|
+
|
|
198
|
+
**Steps:**
|
|
199
|
+
1. [First thing that happens]
|
|
200
|
+
2. [Second thing]
|
|
201
|
+
3. [If condition met] → [Branch action]
|
|
202
|
+
4. [Continue...]
|
|
203
|
+
|
|
204
|
+
**Entities Affected:**
|
|
205
|
+
- [Entity 1]: [What changes]
|
|
206
|
+
- [Entity 2]: [What changes]
|
|
207
|
+
|
|
208
|
+
**Stores to Update:**
|
|
209
|
+
- [Store 1]
|
|
210
|
+
- [Store 2]
|
|
211
|
+
|
|
212
|
+
**Components to Invalidate:**
|
|
213
|
+
- [Component 1]
|
|
214
|
+
- [Component 2]
|
|
215
|
+
|
|
216
|
+
**Duration:** ~[Time estimate]
|
|
217
|
+
|
|
218
|
+
**Example:**
|
|
219
|
+
```javascript
|
|
220
|
+
[Pseudocode of complete cascade]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
**Template Example:**
|
|
226
|
+
|
|
227
|
+
### createSale_complete_cascade
|
|
228
|
+
|
|
229
|
+
**Trigger:** User creates a sale (createSale mutation)
|
|
230
|
+
|
|
231
|
+
**Steps:**
|
|
232
|
+
1. Create sale record (sale_id, user_id, product_id, amount, bonus_volume)
|
|
233
|
+
2. Update user.personal_volume += sale.bonus_volume
|
|
234
|
+
3. Check rank promotion for user
|
|
235
|
+
- If promoted → Update rank, create rank_promotion record
|
|
236
|
+
4. Traverse upline tree (sponsor_id chain, max 10 levels)
|
|
237
|
+
5. For each upline user (level 1-10):
|
|
238
|
+
a. Calculate bonus: sale.bonus_volume * GENERATION_RATES[level]
|
|
239
|
+
b. Update upline_user.bonus_volume += calculated_bonus
|
|
240
|
+
c. Create bonus_volume record (user_id: upline_user.id, from_user_id: user.id, sale_id, volume, level)
|
|
241
|
+
d. Check rank promotion for upline_user
|
|
242
|
+
e. If promoted → Trigger another cascade (leadership bonuses)
|
|
243
|
+
6. Recalculate team_volume for all upline users
|
|
244
|
+
- team_volume = SUM(all_downline.personal_volume + all_downline.team_volume)
|
|
245
|
+
7. Create commission records:
|
|
246
|
+
a. User commission: sale.amount * COMMISSION_RATES[user.rank]
|
|
247
|
+
b. For each upline user:
|
|
248
|
+
- If upline_user.rank > downline_user.rank → Override commission
|
|
249
|
+
- commission = sale.amount * (COMMISSION_RATES[upline_rank] - COMMISSION_RATES[downline_rank])
|
|
250
|
+
|
|
251
|
+
**Entities Affected:**
|
|
252
|
+
- sale: Created
|
|
253
|
+
- user: personal_volume updated, possibly rank updated
|
|
254
|
+
- upline users (1-10): bonus_volume updated, possibly rank updated
|
|
255
|
+
- bonus_volumes: 1-10 records created
|
|
256
|
+
- commissions: 1-11 records created (user + upline)
|
|
257
|
+
- rank_promotions: 0-11 records created (if promotions occurred)
|
|
258
|
+
|
|
259
|
+
**Stores to Update:**
|
|
260
|
+
- SaleStore
|
|
261
|
+
- UserStore (user + 10 upline users = 11 total)
|
|
262
|
+
- BonusVolumeStore
|
|
263
|
+
- CommissionStore
|
|
264
|
+
- RankPromotionStore (conditional)
|
|
265
|
+
|
|
266
|
+
**Components to Invalidate:**
|
|
267
|
+
- User-Dashboard (for user + all upline)
|
|
268
|
+
- Team-View (for all upline)
|
|
269
|
+
- Commission-Report
|
|
270
|
+
|
|
271
|
+
**Duration:** ~500ms - 2s (depending on upline depth and promotions)
|
|
272
|
+
|
|
273
|
+
**Example:**
|
|
274
|
+
```typescript
|
|
275
|
+
async function createSaleWithCascade(input: CreateSaleInput) {
|
|
276
|
+
// 1. Create sale
|
|
277
|
+
const sale = await db.sales.insert({
|
|
278
|
+
user_id: input.user_id,
|
|
279
|
+
product_id: input.product_id,
|
|
280
|
+
amount: input.amount,
|
|
281
|
+
bonus_volume: input.bonus_volume,
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
// 2. Update personal volume
|
|
285
|
+
await db.users.update(input.user_id, {
|
|
286
|
+
personal_volume: db.raw('personal_volume + ?', [sale.bonus_volume])
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// 3. Check rank
|
|
290
|
+
await checkRankPromotion(input.user_id)
|
|
291
|
+
|
|
292
|
+
// 4-6. Upline cascade
|
|
293
|
+
const uplineUsers = await getUplineTree(input.user_id, 10)
|
|
294
|
+
for (const [level, uplineUser] of uplineUsers.entries()) {
|
|
295
|
+
const bonus = sale.bonus_volume * GENERATION_RATES[level]
|
|
296
|
+
|
|
297
|
+
// Update bonus volume
|
|
298
|
+
await db.users.update(uplineUser.id, {
|
|
299
|
+
bonus_volume: db.raw('bonus_volume + ?', [bonus])
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// Create bonus record
|
|
303
|
+
await db.bonus_volumes.insert({
|
|
304
|
+
user_id: uplineUser.id,
|
|
305
|
+
from_user_id: input.user_id,
|
|
306
|
+
sale_id: sale.id,
|
|
307
|
+
volume: bonus,
|
|
308
|
+
level: level + 1,
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// Check promotion
|
|
312
|
+
await checkRankPromotion(uplineUser.id)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 7. Recalc team volume
|
|
316
|
+
for (const uplineUser of uplineUsers) {
|
|
317
|
+
await recalculateTeamVolume(uplineUser.id)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 8. Create commissions
|
|
321
|
+
await createCommissionsForSale(sale.id, input.user_id, uplineUsers)
|
|
322
|
+
|
|
323
|
+
// 9. Invalidate stores
|
|
324
|
+
await invalidateStores([
|
|
325
|
+
'SaleStore',
|
|
326
|
+
'UserStore',
|
|
327
|
+
'BonusVolumeStore',
|
|
328
|
+
'CommissionStore',
|
|
329
|
+
'RankPromotionStore',
|
|
330
|
+
])
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Business Rule Enforcement
|
|
337
|
+
|
|
338
|
+
### [rule_name]
|
|
339
|
+
|
|
340
|
+
**Rule:**
|
|
341
|
+
```
|
|
342
|
+
[Statement of the business rule]
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Enforced Where:**
|
|
346
|
+
- [Location in code]
|
|
347
|
+
|
|
348
|
+
**Validation:**
|
|
349
|
+
```javascript
|
|
350
|
+
[Validation logic]
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Error Handling:**
|
|
354
|
+
```javascript
|
|
355
|
+
[What happens if rule violated]
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
**Template Example:**
|
|
361
|
+
|
|
362
|
+
### minimum_personal_volume_for_commission
|
|
363
|
+
|
|
364
|
+
**Rule:**
|
|
365
|
+
```
|
|
366
|
+
User must have at least $500 in personal_volume in current month to receive commissions
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Enforced Where:**
|
|
370
|
+
- createCommission mutation
|
|
371
|
+
- Monthly commission calculation job
|
|
372
|
+
|
|
373
|
+
**Validation:**
|
|
374
|
+
```javascript
|
|
375
|
+
const currentMonthVolume = await getUserPersonalVolumeForMonth(userId, currentMonth)
|
|
376
|
+
|
|
377
|
+
if (currentMonthVolume < 500) {
|
|
378
|
+
return { eligible: false, reason: 'minimum_volume_not_met' }
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Error Handling:**
|
|
383
|
+
```javascript
|
|
384
|
+
// Don't create commission record
|
|
385
|
+
// Log to audit trail
|
|
386
|
+
await db.commission_holds.insert({
|
|
387
|
+
user_id: userId,
|
|
388
|
+
sale_id: saleId,
|
|
389
|
+
amount: commissionAmount,
|
|
390
|
+
hold_reason: 'minimum_volume_not_met',
|
|
391
|
+
current_volume: currentMonthVolume,
|
|
392
|
+
required_volume: 500,
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// User will see in dashboard: "Commission on hold - reach $500 personal volume"
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Aggregation Formulas
|
|
401
|
+
|
|
402
|
+
### [aggregation_name]
|
|
403
|
+
|
|
404
|
+
**Formula:**
|
|
405
|
+
```sql
|
|
406
|
+
[SQL aggregation]
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**Recalculation Triggers:**
|
|
410
|
+
- [When to recalculate]
|
|
411
|
+
|
|
412
|
+
**Performance:**
|
|
413
|
+
- Estimated rows: [N]
|
|
414
|
+
- Estimated time: [Duration]
|
|
415
|
+
- Optimization: [Any indexes or caching]
|
|
416
|
+
|
|
417
|
+
**Example:**
|
|
418
|
+
```typescript
|
|
419
|
+
[Implementation code]
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
**Template Example:**
|
|
425
|
+
|
|
426
|
+
### team_volume_calculation
|
|
427
|
+
|
|
428
|
+
**Formula:**
|
|
429
|
+
```sql
|
|
430
|
+
WITH RECURSIVE downline AS (
|
|
431
|
+
SELECT id, personal_volume, team_volume, 0 as depth
|
|
432
|
+
FROM users
|
|
433
|
+
WHERE id = $user_id
|
|
434
|
+
UNION ALL
|
|
435
|
+
SELECT u.id, u.personal_volume, u.team_volume, d.depth + 1
|
|
436
|
+
FROM users u
|
|
437
|
+
JOIN downline d ON u.sponsor_id = d.id
|
|
438
|
+
WHERE d.depth < 10
|
|
439
|
+
)
|
|
440
|
+
SELECT SUM(personal_volume) as total_team_volume
|
|
441
|
+
FROM downline
|
|
442
|
+
WHERE id != $user_id;
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Recalculation Triggers:**
|
|
446
|
+
- When any downline user makes a sale (personal_volume changes)
|
|
447
|
+
- When downline structure changes (user changes sponsor)
|
|
448
|
+
- When downline user gets promoted (affects team calculations)
|
|
449
|
+
|
|
450
|
+
**Performance:**
|
|
451
|
+
- Estimated rows: 1-1000 (depending on team size)
|
|
452
|
+
- Estimated time: 10-500ms
|
|
453
|
+
- Optimization:
|
|
454
|
+
- Index on sponsor_id
|
|
455
|
+
- Cache team_volume, recalc only on changes
|
|
456
|
+
- Use materialized view for large teams (>100 members)
|
|
457
|
+
|
|
458
|
+
**Example:**
|
|
459
|
+
```typescript
|
|
460
|
+
async function recalculateTeamVolume(userId: string) {
|
|
461
|
+
const result = await db.raw(`
|
|
462
|
+
WITH RECURSIVE downline AS (
|
|
463
|
+
SELECT id, personal_volume, team_volume, 0 as depth
|
|
464
|
+
FROM users
|
|
465
|
+
WHERE id = $1
|
|
466
|
+
UNION ALL
|
|
467
|
+
SELECT u.id, u.personal_volume, u.team_volume, d.depth + 1
|
|
468
|
+
FROM users u
|
|
469
|
+
JOIN downline d ON u.sponsor_id = d.id
|
|
470
|
+
WHERE d.depth < 10
|
|
471
|
+
)
|
|
472
|
+
SELECT SUM(personal_volume) as total_team_volume
|
|
473
|
+
FROM downline
|
|
474
|
+
WHERE id != $1
|
|
475
|
+
`, [userId])
|
|
476
|
+
|
|
477
|
+
const teamVolume = result.rows[0].total_team_volume || 0
|
|
478
|
+
|
|
479
|
+
await db.users.update(userId, { team_volume: teamVolume })
|
|
480
|
+
|
|
481
|
+
return teamVolume
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Constants / Configuration
|
|
488
|
+
|
|
489
|
+
### Rank Thresholds (Personal Volume)
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const RANK_THRESHOLDS = {
|
|
493
|
+
'Associate': 0,
|
|
494
|
+
'Silver': 1000,
|
|
495
|
+
'Gold': 5000,
|
|
496
|
+
'Platinum': 10000,
|
|
497
|
+
'Diamond': 25000,
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Team Volume Thresholds (Alternative Promotion Path)
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
const TEAM_RANK_THRESHOLDS = {
|
|
505
|
+
'Associate': 0,
|
|
506
|
+
'Silver': 5000,
|
|
507
|
+
'Gold': 25000,
|
|
508
|
+
'Platinum': 100000,
|
|
509
|
+
'Diamond': 500000,
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Generation Bonus Rates
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
const GENERATION_RATES = [
|
|
517
|
+
1.0, // Level 1 (direct sponsor): 100%
|
|
518
|
+
1.0, // Level 2: 100%
|
|
519
|
+
0.5, // Level 3: 50%
|
|
520
|
+
0.5, // Level 4: 50%
|
|
521
|
+
0.25, // Level 5: 25%
|
|
522
|
+
0.25, // Level 6: 25%
|
|
523
|
+
0.25, // Level 7: 25%
|
|
524
|
+
0.25, // Level 8: 25%
|
|
525
|
+
0.25, // Level 9: 25%
|
|
526
|
+
0.25, // Level 10: 25%
|
|
527
|
+
]
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Commission Rates (by Rank)
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
const COMMISSION_RATES = {
|
|
534
|
+
'Associate': 0.10, // 10%
|
|
535
|
+
'Silver': 0.15, // 15%
|
|
536
|
+
'Gold': 0.20, // 20%
|
|
537
|
+
'Platinum': 0.25, // 25%
|
|
538
|
+
'Diamond': 0.30, // 30%
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## Edge Cases
|
|
545
|
+
|
|
546
|
+
### [edge_case_name]
|
|
547
|
+
|
|
548
|
+
**Scenario:**
|
|
549
|
+
```
|
|
550
|
+
[Description of edge case]
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Handling:**
|
|
554
|
+
```javascript
|
|
555
|
+
[How to handle it]
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**Example:**
|
|
559
|
+
```
|
|
560
|
+
[Concrete example]
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
**Template Example:**
|
|
566
|
+
|
|
567
|
+
### circular_sponsor_reference
|
|
568
|
+
|
|
569
|
+
**Scenario:**
|
|
570
|
+
```
|
|
571
|
+
User A sponsors User B sponsors User C sponsors User A (circular reference)
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Handling:**
|
|
575
|
+
```javascript
|
|
576
|
+
// Prevent at creation time
|
|
577
|
+
async function updateUserSponsor(userId: string, newSponsorId: string) {
|
|
578
|
+
// Check if new sponsor is in user's downline
|
|
579
|
+
const downline = await getDownlineTree(userId, 100)
|
|
580
|
+
const downlineIds = downline.map(u => u.id)
|
|
581
|
+
|
|
582
|
+
if (downlineIds.includes(newSponsorId)) {
|
|
583
|
+
throw new Error('Cannot set sponsor - would create circular reference')
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Safe to update
|
|
587
|
+
await db.users.update(userId, { sponsor_id: newSponsorId })
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Also prevent in upline traversal (infinite loop protection)
|
|
591
|
+
async function getUplineTree(userId: string, maxDepth: number) {
|
|
592
|
+
const visited = new Set<string>()
|
|
593
|
+
const upline = []
|
|
594
|
+
|
|
595
|
+
let currentId = userId
|
|
596
|
+
let depth = 0
|
|
597
|
+
|
|
598
|
+
while (depth < maxDepth) {
|
|
599
|
+
const user = await getUser(currentId)
|
|
600
|
+
if (!user.sponsor_id) break
|
|
601
|
+
if (visited.has(user.sponsor_id)) {
|
|
602
|
+
console.error(`Circular reference detected: ${user.sponsor_id}`)
|
|
603
|
+
break // Stop traversal
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
visited.add(user.sponsor_id)
|
|
607
|
+
upline.push(await getUser(user.sponsor_id))
|
|
608
|
+
currentId = user.sponsor_id
|
|
609
|
+
depth++
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return upline
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
**Example:**
|
|
617
|
+
```
|
|
618
|
+
User ID: 123
|
|
619
|
+
Sponsor ID: 456
|
|
620
|
+
Sponsor's Sponsor ID: 789
|
|
621
|
+
Sponsor's Sponsor's Sponsor ID: 123 // ❌ Circular!
|
|
622
|
+
|
|
623
|
+
Prevention: Reject update when setting sponsor_id = 789
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## Testing Scenarios
|
|
629
|
+
|
|
630
|
+
### [scenario_name]
|
|
631
|
+
|
|
632
|
+
**Setup:**
|
|
633
|
+
```
|
|
634
|
+
[Initial state]
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Action:**
|
|
638
|
+
```
|
|
639
|
+
[What user does]
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
**Expected Result:**
|
|
643
|
+
```
|
|
644
|
+
[What should happen - all cascades]
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**Assertions:**
|
|
648
|
+
```javascript
|
|
649
|
+
[Test assertions]
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
**Template Example:**
|
|
655
|
+
|
|
656
|
+
### sale_triggers_multiple_promotions
|
|
657
|
+
|
|
658
|
+
**Setup:**
|
|
659
|
+
```
|
|
660
|
+
User A (Associate, personal_volume: 950)
|
|
661
|
+
↳ Sponsor B (Silver, personal_volume: 4800, team_volume: 950)
|
|
662
|
+
↳ Sponsor C (Gold, team_volume: 5750)
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
**Action:**
|
|
666
|
+
```
|
|
667
|
+
User A makes $100 sale (bonus_volume: 100)
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
**Expected Result:**
|
|
671
|
+
```
|
|
672
|
+
1. Sale created ($100)
|
|
673
|
+
2. User A:
|
|
674
|
+
- personal_volume: 950 → 1050
|
|
675
|
+
- Promoted: Associate → Silver (threshold: 1000)
|
|
676
|
+
- rank_promotion record created
|
|
677
|
+
3. Sponsor B:
|
|
678
|
+
- bonus_volume: +100
|
|
679
|
+
- team_volume: 950 → 1050
|
|
680
|
+
- personal_volume: 4800 + 100 = 4900
|
|
681
|
+
- Still Silver (needs 5000 for Gold)
|
|
682
|
+
4. Sponsor C:
|
|
683
|
+
- bonus_volume: +100 (generation 2)
|
|
684
|
+
- team_volume: 5750 → 5850
|
|
685
|
+
- Still Gold
|
|
686
|
+
5. Bonus records created:
|
|
687
|
+
- User B gets $100 (level 1, 100%)
|
|
688
|
+
- User C gets $100 (level 2, 100%)
|
|
689
|
+
6. Commissions created:
|
|
690
|
+
- User A: $100 * 15% = $15 (Silver rate - promoted mid-sale)
|
|
691
|
+
- User B: $100 * (15% - 0%) = $15 (override)
|
|
692
|
+
- User C: $100 * (20% - 15%) = $5 (override)
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
**Assertions:**
|
|
696
|
+
```typescript
|
|
697
|
+
test('sale triggers multiple promotions', async () => {
|
|
698
|
+
// Setup
|
|
699
|
+
const userA = await createUser({ rank: 'Associate', personal_volume: 950 })
|
|
700
|
+
const userB = await createUser({ rank: 'Silver', personal_volume: 4800, team_volume: 950 })
|
|
701
|
+
const userC = await createUser({ rank: 'Gold', team_volume: 5750 })
|
|
702
|
+
await updateUser(userA.id, { sponsor_id: userB.id })
|
|
703
|
+
await updateUser(userB.id, { sponsor_id: userC.id })
|
|
704
|
+
|
|
705
|
+
// Action
|
|
706
|
+
await createSale({ user_id: userA.id, amount: 100, bonus_volume: 100 })
|
|
707
|
+
|
|
708
|
+
// Assertions
|
|
709
|
+
const updatedA = await getUser(userA.id)
|
|
710
|
+
expect(updatedA.personal_volume).toBe(1050)
|
|
711
|
+
expect(updatedA.rank).toBe('Silver')
|
|
712
|
+
|
|
713
|
+
const updatedB = await getUser(userB.id)
|
|
714
|
+
expect(updatedB.bonus_volume).toBe(100)
|
|
715
|
+
expect(updatedB.team_volume).toBe(1050)
|
|
716
|
+
|
|
717
|
+
const updatedC = await getUser(userC.id)
|
|
718
|
+
expect(updatedC.team_volume).toBe(5850)
|
|
719
|
+
|
|
720
|
+
const bonuses = await getBonusVolumes({ sale_id: sale.id })
|
|
721
|
+
expect(bonuses).toHaveLength(2)
|
|
722
|
+
expect(bonuses[0].user_id).toBe(userB.id)
|
|
723
|
+
expect(bonuses[0].volume).toBe(100)
|
|
724
|
+
expect(bonuses[1].user_id).toBe(userC.id)
|
|
725
|
+
expect(bonuses[1].volume).toBe(100)
|
|
726
|
+
|
|
727
|
+
const commissions = await getCommissions({ sale_id: sale.id })
|
|
728
|
+
expect(commissions).toHaveLength(3)
|
|
729
|
+
expect(commissions.find(c => c.user_id === userA.id)?.amount).toBe(15)
|
|
730
|
+
expect(commissions.find(c => c.user_id === userB.id)?.amount).toBe(15)
|
|
731
|
+
expect(commissions.find(c => c.user_id === userC.id)?.amount).toBe(5)
|
|
732
|
+
})
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
## Implementation Notes
|
|
738
|
+
|
|
739
|
+
[Add any additional notes here about:
|
|
740
|
+
- Performance considerations
|
|
741
|
+
- Caching strategies
|
|
742
|
+
- Queue vs immediate processing
|
|
743
|
+
- Transaction boundaries
|
|
744
|
+
- Error handling strategies
|
|
745
|
+
- Monitoring / alerting
|
|
746
|
+
]
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
**This document is maintained manually. Update when business rules change.**
|