refactor: improve cycle generation code quality and documentation
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- Remove Process.sleep calls from integration tests (tests run synchronously in SQL sandbox) - Improve error handling: membership_fee_type_not_found now returns changeset error instead of just logging - Clarify partial_failure documentation: successful_cycles are not persisted on rollback - Update documentation: joined_at → join_date, left_at → exit_date - Document PostgreSQL advisory locks per member (not whole table lock) - Document gap handling: explicitly deleted cycles are not recreated
This commit is contained in:
parent
e6ac5d1ab1
commit
82897d5cd3
5 changed files with 53 additions and 51 deletions
|
|
@ -153,8 +153,8 @@ lib/
|
|||
|
||||
**Existing Fields Used:**
|
||||
|
||||
- `joined_at` - For calculating membership fee start
|
||||
- `left_at` - For limiting cycle generation
|
||||
- `join_date` - For calculating membership fee start
|
||||
- `exit_date` - For limiting cycle generation
|
||||
- These fields must remain member fields and should not be replaced by custom fields in the future
|
||||
|
||||
### Settings Integration
|
||||
|
|
@ -186,8 +186,9 @@ lib/
|
|||
|
||||
- Calculate which cycles should exist for a member
|
||||
- Generate missing cycles
|
||||
- Respect membership_fee_start_date and left_at boundaries
|
||||
- Respect membership_fee_start_date and exit_date boundaries
|
||||
- Skip existing cycles (idempotent)
|
||||
- Use PostgreSQL advisory locks per member to prevent race conditions
|
||||
|
||||
**Triggers:**
|
||||
|
||||
|
|
@ -199,17 +200,20 @@ lib/
|
|||
**Algorithm Steps:**
|
||||
|
||||
1. Retrieve member with membership fee type and dates
|
||||
2. Determine first cycle start (based on membership_fee_start_date)
|
||||
3. Calculate all cycle starts from first to today (or left_at)
|
||||
4. Query existing cycles for member
|
||||
5. Generate missing cycles with current membership fee type's amount
|
||||
6. Insert new cycles (batch operation)
|
||||
2. Determine generation start point:
|
||||
- If NO cycles exist: Start from `membership_fee_start_date` (or calculated from `join_date`)
|
||||
- If cycles exist: Start from the cycle AFTER the last existing one
|
||||
3. Generate all cycle starts from the determined start point to today (or `exit_date`)
|
||||
4. Create new cycles with current membership fee type's amount
|
||||
5. Use PostgreSQL advisory locks per member to prevent race conditions
|
||||
|
||||
**Edge Case Handling:**
|
||||
|
||||
- If membership_fee_start_date is NULL: Calculate from joined_at + global setting
|
||||
- If left_at is set: Stop generation at left_at
|
||||
- If membership_fee_start_date is NULL: Calculate from join_date + global setting
|
||||
- If exit_date is set: Stop generation at exit_date
|
||||
- If membership fee type changes: Handled separately by regeneration logic
|
||||
- **Gap Handling:** If cycles were explicitly deleted (gaps exist), they are NOT recreated.
|
||||
The generator always continues from the cycle AFTER the last existing cycle, regardless of gaps.
|
||||
|
||||
### Calendar Cycle Calculations
|
||||
|
||||
|
|
@ -381,7 +385,7 @@ lib/
|
|||
**AC-M-1:** Member has membership_fee_type_id field (NOT NULL with default)
|
||||
**AC-M-2:** Member has membership_fee_start_date field (nullable)
|
||||
**AC-M-3:** New members get default membership fee type from global setting
|
||||
**AC-M-4:** membership_fee_start_date auto-set based on joined_at and global setting
|
||||
**AC-M-4:** membership_fee_start_date auto-set based on join_date and global setting
|
||||
**AC-M-5:** Admin can manually override membership_fee_start_date
|
||||
**AC-M-6:** Cannot change to membership fee type with different interval (MVP)
|
||||
|
||||
|
|
@ -391,7 +395,7 @@ lib/
|
|||
**AC-CG-2:** Cycles generated when member created (via change hook)
|
||||
**AC-CG-3:** Scheduled job generates missing cycles daily
|
||||
**AC-CG-4:** Generation respects membership_fee_start_date
|
||||
**AC-CG-5:** Generation stops at left_at if member exited
|
||||
**AC-CG-5:** Generation stops at exit_date if member exited
|
||||
**AC-CG-6:** Generation is idempotent (skips existing cycles)
|
||||
**AC-CG-7:** Cycles align to calendar boundaries (1st of month/quarter/half/year)
|
||||
**AC-CG-8:** Amount comes from membership_fee_type at generation time
|
||||
|
|
@ -472,8 +476,9 @@ lib/
|
|||
- Correct cycle_start calculation for all interval types
|
||||
- Correct cycle count from start to end date
|
||||
- Respects membership_fee_start_date boundary
|
||||
- Respects left_at boundary
|
||||
- Respects exit_date boundary
|
||||
- Skips existing cycles (idempotent)
|
||||
- Does not fill gaps when cycles were deleted
|
||||
- Handles edge dates (year boundaries, leap years)
|
||||
|
||||
**Calendar Cycles Tests:**
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ This document provides a comprehensive overview of the Membership Fees system. I
|
|||
```
|
||||
- membership_fee_type_id (FK → membership_fee_types.id, NOT NULL, default from settings)
|
||||
- membership_fee_start_date (Date, nullable) - When to start generating membership fees
|
||||
- left_at (Date, nullable) - Exit date (existing)
|
||||
- exit_date (Date, nullable) - Exit date (existing)
|
||||
```
|
||||
|
||||
**Logic for membership_fee_start_date:**
|
||||
|
|
@ -167,16 +167,17 @@ value: UUID (Required) - Default membership fee type for new members
|
|||
|
||||
**Algorithm:**
|
||||
|
||||
Lock the whole cycle table for the duration of the algorithm
|
||||
Use PostgreSQL advisory locks per member to prevent race conditions
|
||||
|
||||
1. Get `member.membership_fee_start_date` and member's membership fee type
|
||||
2. Generate cycles until today (or `left_at` if present):
|
||||
- If no cycle exists:
|
||||
- Generate all cycles from `membership_fee_start_date`
|
||||
- else:
|
||||
- Generate all cycles from last existing cycle
|
||||
- use the interval to generate the cycles
|
||||
3. Set `amount` to current membership fee type's amount
|
||||
2. Determine generation start point:
|
||||
- If NO cycles exist: Start from `membership_fee_start_date`
|
||||
- If cycles exist: Start from the cycle AFTER the last existing one
|
||||
3. Generate cycles until today (or `exit_date` if present):
|
||||
- Use the interval to generate the cycles
|
||||
- **Note:** If cycles were explicitly deleted (gaps exist), they are NOT recreated.
|
||||
The generator always continues from the cycle AFTER the last existing cycle.
|
||||
4. Set `amount` to current membership fee type's amount
|
||||
|
||||
**Example (Yearly):**
|
||||
|
||||
|
|
@ -246,7 +247,7 @@ suspended → unpaid
|
|||
|
||||
**Logic:**
|
||||
|
||||
- Cycles only generated until `member.left_at`
|
||||
- Cycles only generated until `member.exit_date`
|
||||
- Existing cycles remain visible
|
||||
- Unpaid exit cycle can be marked as "suspended"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue