# Daily Reward Calculation Implementation Status

## Overview
This document provides a status update on the implementation of daily reward calculation for active investments, based on the requirements in `docs/requirements-analysis.md`.

## Requirements Summary
According to the requirements, there are 3 main parts:
1. **Change pending investment to active** - After 1 day (first reward calculation starts at 00:00 UTC+8 on the day after investment)
2. **Calculate daily rewards** - For active investments, with rewards held for 24 hours before being credited
3. **Complete investment records** - When investment duration is up

---

## Implementation Status

### ✅ Part 1: Pending → Active Status Transition
**Status: IMPLEMENTED** ✓

**Location:** `backend/apps/investments/management/commands/calculate_rewards.py`

**Implementation Details:**
- Investments are created with `STATUS_PENDING` (line 102 in `serializers.py`)
- The `calculate_rewards` command processes both `STATUS_PENDING` and `STATUS_ACTIVE` investments (line 40)
- First reward calculation check: `if target_date < (investment.start_date + timedelta(days=1)).date()` (line 50)
  - This ensures first reward is calculated on the day after investment
- Status transition happens when first reward is calculated (lines 96-100):
  ```python
  investment.status = (
      Investment.STATUS_COMPLETED
      if target_date >= investment.end_date.date()
      else Investment.STATUS_ACTIVE
  )
  ```

**Verification:**
- ✅ Investments start as `STATUS_PENDING`
- ✅ Status changes to `STATUS_ACTIVE` when first reward is calculated
- ✅ First reward is calculated on the day after investment (not on the same day)

---

### ⚠️ Part 2: Daily Reward Calculation
**Status: PARTIALLY IMPLEMENTED** ⚠️

**What's Implemented:**
- ✅ Daily reward calculation command exists (`calculate_rewards.py`)
- ✅ Rewards are calculated based on investment tier and daily reward rate
- ✅ Rewards are stored in `Reward` model with `reward_date`, `calculated_at`, and `distributed_at` fields
- ✅ Rewards are credited to user's balance immediately
- ✅ Commission calculation happens immediately after reward calculation
- ✅ Idempotent - running command twice doesn't duplicate rewards

**What's Missing:**
- ❌ **24-hour hold period**: Rewards are distributed immediately (`distributed_at=now` on line 80)
  - **Requirement:** "Rewards are held for 24 hours before being credited"
  - **Current behavior:** Rewards are calculated and credited in the same run
  - **Expected behavior:** 
    - Day 1: Calculate reward for date X, set `calculated_at`, but `distributed_at` = NULL
    - Day 2: Distribute rewards calculated 24 hours ago (where `calculated_at` < now - 24 hours and `distributed_at` is NULL)

**Code Location:**
- Reward creation: `backend/apps/investments/management/commands/calculate_rewards.py` lines 74-82
- Reward crediting: `backend/apps/investments/management/commands/calculate_rewards.py` lines 87-92

**Timing Verification Needed:**
- ⚠️ Need to verify timing matches UTC+8 timezone requirement
- ⚠️ Need to verify calculation happens at 00:00 UTC+8 (depends on cronjob configuration)

---

### ✅ Part 3: Complete Investment Records
**Status: IMPLEMENTED** ✓

**Location:** `backend/apps/investments/management/commands/calculate_rewards.py` lines 96-100

**Implementation Details:**
- When `target_date >= investment.end_date.date()`, investment status is set to `STATUS_COMPLETED`
- Last day's reward is still calculated and distributed
- Commission for the last day's reward is calculated as usual

**Verification:**
- ✅ Investment status changes to `STATUS_COMPLETED` when duration expires
- ✅ Last day's reward is issued
- ✅ Commission for last day is calculated
- ✅ Completed investments are excluded from future reward calculations (line 40 filters out completed)

---

## Detailed Code Analysis

### Investment Creation Flow
1. **User creates investment** → `InvestmentSerializer.create()` (serializers.py:51-118)
   - Sets `status=STATUS_PENDING`
   - Sets `start_date=timezone.now()`
   - Sets `end_date=start_date + timedelta(days=duration)`
   - Deducts balance from user

### Reward Calculation Flow
1. **Cronjob runs** → `calculate_rewards` command
2. **Filters eligible investments** (line 39-43):
   - Status: `PENDING` or `ACTIVE`
   - `start_date.date() <= target_date <= end_date.date()`
3. **Skips if before first reward day** (line 50):
   - `if target_date < (investment.start_date + timedelta(days=1)).date(): continue`
4. **Creates reward** (line 75-82):
   - Calculates amount: `investment.amount * daily_reward_rate / 100`
   - Sets `distributed_at=now` ← **ISSUE: Should be NULL initially**
5. **Credits user immediately** (line 87-92) ← **ISSUE: Should wait 24 hours**
6. **Creates commissions** (line 94)
7. **Updates investment status** (line 96-100):
   - `COMPLETED` if `target_date >= end_date.date()`
   - `ACTIVE` otherwise

---

## Issues Identified

### Issue 1: Missing 24-Hour Hold Period ⚠️ **CRITICAL**
**Current Behavior:**
- Rewards are calculated and distributed immediately in the same command execution
- `distributed_at` is set to `now` when reward is created

**Required Behavior:**
- Rewards should be calculated on day N
- Rewards should be distributed on day N+1 (24 hours later)
- Need separate logic:
  1. **Calculate rewards** for eligible investments (where reward_date = today - 1 day)
  2. **Distribute rewards** for rewards calculated 24+ hours ago (where `calculated_at` < now - 24 hours AND `distributed_at` IS NULL)

**Impact:**
- Users receive rewards immediately instead of after 24-hour hold period
- Does not match business requirements

**Solution Required:**
- Modify `calculate_rewards.py` to:
  1. Calculate rewards without setting `distributed_at` (or set to NULL)
  2. Add separate distribution logic that credits rewards where `calculated_at` < now - 24 hours
  3. Or create a separate command `distribute_rewards` that runs after `calculate_rewards`

### Issue 2: Timezone Configuration ⚠️ **NEEDS FIX**
**Requirement:**
- All calculations use UTC+8 timezone
- Calculation happens at 00:00 UTC+8

**Current Implementation:**
- `TIME_ZONE = "UTC"` in `backend/config/settings_base.py` (line 85)
- Uses `timezone.now()` which uses Django's `TIME_ZONE` setting (currently UTC)
- Cronjob timing needs to be verified (should run at 00:00 UTC+8)

**Impact:**
- Calculations are happening in UTC instead of UTC+8
- This causes timing misalignment with requirements

**Action Required:**
- Change `TIME_ZONE` to `"Asia/Shanghai"` or `"Asia/Hong_Kong"` (both are UTC+8)
- Or use timezone-aware datetime operations with UTC+8 offset
- Verify cronjob configuration runs at correct time (00:00 UTC+8)

---

## Test Coverage

### Existing Tests
✅ `test_calculate_rewards.py` covers:
- Reward creation and commission calculation
- Status transitions (PENDING → ACTIVE → COMPLETED)
- Idempotency
- Edge cases (no uplines, partial referral chains)

### Missing Tests
❌ Tests for 24-hour hold period (not applicable until implemented)
❌ Tests for timezone handling
❌ Tests for reward distribution separate from calculation

---

## Recommendations

### Priority 1: Implement 24-Hour Hold Period
1. **Option A (Recommended):** Modify `calculate_rewards.py` to have two phases:
   - Phase 1: Calculate rewards (set `calculated_at`, leave `distributed_at` = NULL)
   - Phase 2: Distribute rewards (where `calculated_at` < now - 24 hours AND `distributed_at` IS NULL)
   
2. **Option B:** Create separate command `distribute_rewards.py`:
   - `calculate_rewards` only calculates (no crediting)
   - `distribute_rewards` distributes rewards calculated 24+ hours ago
   - Cronjob runs both commands

### Priority 2: Verify Timezone Configuration
- Check Django `TIME_ZONE` setting
- Verify cronjob timing
- Add timezone-aware tests

### Priority 3: Update Tests
- Add tests for 24-hour hold period
- Add tests for timezone handling
- Add tests for reward distribution logic

---

## Summary

| Pending → Active transition | ✅ Implemented | Logic in `CalculateRewardsTask` |
| Daily reward calculation | ✅ Implemented | `CalculateRewardsTask` (INV001) |
| Investment completion | ✅ Implemented | Logic in `CalculateRewardsTask` |
| 24-hour hold period | ✅ Implemented | Achieved via separate `DistributeRewardsTask` |
| Cronjob Dependency | ✅ Implemented | `DistributeRewardsTask` checks `CalculateRewardsTask` success |

**Overall Status:** 3/3 parts fully implemented.
- **Split Strategy**:
  1. `calculate-rewards` (INV001): Calculates rewards for target date.
  2. `distribute-rewards` (INV002): Distributes rewards for target date (depends on INV001).
- **Dependency**: `DistributeRewardsTask` enforces that `CalculateRewardsTask` must be successful for the same date.
- **Timing**: Run both jobs for Date X. Rewards for Date X are calculated, then distributed.
  - To match requirements ("Invest 5th -> Calc 7th for 6th"): Run jobs on the 7th with argument "6th".

