Skip to main content

CycleInvestment Model Analysis

Executive Summary​

With the introduction of comprehensive double-entry accounting via journal entries, the CycleInvestment model has become largely redundant. Most of its original purposes are now better served by the journal entry system.

Current State Analysis​

What CycleInvestment Currently Stores​

model CycleInvestment {
id Int // Primary key
cycleId Int // Which cycle
investorId Int // Which user
roleAtInvestment UserRole // Role snapshot
amount Decimal // Investment amount
sharePercentage Decimal // Their % of cycle
payoutStrategyId Int? // Payout preference
investedAt DateTime // When they joined
}

Original Design Intentions​

  1. Snapshot Model: Capture point-in-time participation when user opts into cycle
  2. Share Calculation: Track user's percentage of total cycle capital
  3. Payout Strategy: Link to user's profit distribution preference
  4. Role Tracking: Record user's role at time of investment (for profit distribution)
  5. Investment History: Show user's participation across cycles

Redundancy with Journal Entry System​

What Journal Entries Now Provide​

CycleInvestment FieldJournal Entry EquivalentStatus
amountAccount 2130 (User Capital Deployed) balance by userId + cycleIdREDUNDANT
roleAtInvestmentProfitDistribution.roleAtDistributionPARTIALLY REDUNDANT
sharePercentageCan be calculated from capital deployedREDUNDANT
investedAtJournal entry entryDate where capital moved to 2130REDUNDANT

What's NOT Redundant​

  1. payoutStrategyId - User's profit distribution preference (compound vs payout)

    • Not tracked in journal entries
    • Needed for profit distribution decision
  2. Participation Signal - Easy query: "Who is in this cycle?"

    • Could query journal entries but slower
    • Convenience for UI display

Problems with Current Implementation​

1. Data Inconsistency Risk​

Problem: Two sources of truth for the same data

  • CycleInvestment.amount says one thing
  • Journal entries (account 2130) say another

Example: Your Cycle #1 case

  • You have ProfitDistribution for Cycle #1 (you participated)
  • You have NO CycleInvestment for Cycle #1 (missing record)
  • Journal entries show your deposits and distributions
  • Dashboard fails because it relies on CycleInvestment

2. Manual Synchronisation Required​

When users participate in cycles:

// Current flow creates TWO records:
1. Create CycleInvestment record (participate API)
2. Create journal entries moving capital (participate API)

// Risk: One succeeds, other fails = inconsistency

3. Historical Data Gaps​

  • Legacy investments may have journal entries but no CycleInvestment
  • Migrations or corrections can miss creating CycleInvestment records
  • Results in incomplete user history displays

Current Usage Analysis​

Where CycleInvestment is REQUIRED​

  1. Payout Strategy Selection (/api/investments/[id]/payout-strategy)

    • Links user's strategy preference to specific cycle participation
    • NEEDED: No alternative in journal system
  2. Dashboard "My Investments" Tab (/app/dashboard/page.tsx)

    • Shows user's cycle participations with payout strategies
    • Could be replaced with ProfitDistribution + journal query
    • CONVENIENCE: Makes queries simpler
  3. Active Cycles Count (/api/user/balance-stats)

    • Counts cycles where user has deployed capital
    • Could query journal entries with status filter
    • CONVENIENCE: Faster query

Where CycleInvestment is REDUNDANT​

  1. Share Percentage Calculation (computed from capital deployed)
  2. Investment Amount Tracking (account 2130 journal entries)
  3. Investment Timing (journal entry dates)
  4. Role Tracking (ProfitDistribution has this)

Design Recommendations​

Approach: Remove CycleInvestment, use journal entries as source of truth

Changes Needed:

  1. Move Payout Strategy to User Level

    model User {
    defaultPayoutStrategyId Int?
    payoutStrategies PayoutStrategy[]
    }

    model PayoutStrategy {
    // Keep as is, but reference user globally
    // User can override per-opportunity if needed
    }
  2. Replace Dashboard Queries

    // OLD: Find CycleInvestments
    const investments = await prisma.cycleInvestment.findMany({
    where: { investorId: userId }
    })

    // NEW: Find ProfitDistributions (proof of participation)
    const investments = await prisma.profitDistribution.findMany({
    where: { userId: userId }
    })
    // OR query journal entries for 2130 movements
  3. Calculate Active Cycles from Journal Entries

    // Count distinct cycles with deployed capital (2130 balance > 0)
    const activeCycles = await prisma.journalEntryLine.groupBy({
    by: ['cycleId'],
    where: {
    userId: userId,
    accountCode: '2130',
    journalEntry: { status: 'posted' }
    },
    having: {
    balance: { gt: 0 }
    }
    })

Pros:

  • Single source of truth (journal entries)
  • No synchronisation overhead
  • Consistent with accounting best practices
  • Automatically handles all scenarios (legacy, corrections, etc.)

Cons:

  • More complex queries
  • Need to migrate existing payout strategy associations
  • UI changes required

Option B: Keep CycleInvestment as Snapshot Cache (Current)​

Approach: Keep CycleInvestment but treat as denormalised cache

Changes Needed:

  1. Make it Clear it's Derived Data

    model CycleInvestment {
    // ... existing fields ...

    @@index([cycleId, investorId])
    @@map("cycle_investments")
    // Add comment: "Denormalised cache - journal entries are source of truth"
    }
  2. Add Reconciliation

    • Background job to sync CycleInvestment from journal entries
    • Detect and fix missing records (like your Cycle #1)
  3. Add Safeguards

    • Validation that CycleInvestment.amount matches journal balance
    • Alerts when they diverge

Pros:

  • Simpler queries (for now)
  • Minimal code changes
  • Keeps current UI working

Cons:

  • Ongoing synchronisation complexity
  • Risk of data inconsistencies
  • Technical debt accumulation
  • Still need to fix missing historical records

Option C: Hybrid - Minimal CycleInvestment​

Approach: Reduce CycleInvestment to only what's truly needed

New Schema:

model CycleParticipation {
id Int @id @default(autoincrement())
cycleId Int
userId Int
payoutStrategyId Int? // Only field we really need
createdAt DateTime @default(now())

cycle Cycle
user User
payoutStrategy PayoutStrategy?

@@unique([cycleId, userId])
@@map("cycle_participations")
}

Remove: amount, sharePercentage, roleAtInvestment, investedAt (all derivable)

Pros:

  • Keeps essential functionality (payout strategy)
  • Reduces redundancy
  • Cleaner data model

Cons:

  • Requires migration
  • Still need to query journal entries for amounts/shares

Migration Path (If Option A Chosen)​

Phase 1: Create Missing Records​

-- Find users with ProfitDistributions but no CycleInvestment
INSERT INTO cycle_investments (cycle_id, investor_id, role_at_investment, amount, share_percentage, invested_at)
SELECT
pd.cycle_id,
pd.user_id,
pd.role_at_distribution,
pd.investment_amount,
-- Calculate share from investment_amount / cycle total
pd.investment_amount / c.starting_capital,
c.start_date
FROM profit_distributions pd
JOIN cycles c ON c.id = pd.cycle_id
LEFT JOIN cycle_investments ci ON ci.cycle_id = pd.cycle_id AND ci.investor_id = pd.user_id
WHERE ci.id IS NULL;

Phase 2: Add User-Level Payout Strategies​

ALTER TABLE users ADD COLUMN default_payout_strategy_id INT REFERENCES payout_strategies(id);

-- Migrate existing strategy preferences
UPDATE users u
SET default_payout_strategy_id = (
SELECT payout_strategy_id
FROM cycle_investments ci
WHERE ci.investor_id = u.id
AND payout_strategy_id IS NOT NULL
LIMIT 1
);

Phase 3: Update Code to Use Journal Entries​

  • Update /api/investments to query ProfitDistribution
  • Update dashboard to derive data from journal entries
  • Add convenience functions in lib/accounting.ts

Phase 4: Deprecation​

  • Mark CycleInvestment as deprecated in schema comments
  • Stop creating new records
  • Keep table for historical reference

Recommendation​

Adopt Option A: Deprecate CycleInvestment

Rationale:

  1. Journal entries are already the source of truth for all financial data
  2. Maintaining dual systems creates ongoing complexity and risk
  3. Your Cycle #1 issue demonstrates the fragility of the current approach
  4. Aligns with double-entry accounting best practices
  5. Simplifies future development (one less table to think about)

Immediate Action:

  1. Fix your Cycle #1 missing CycleInvestment record
  2. Create reconciliation script to find other gaps
  3. Plan migration timeline for Option A

Timeline:

  • Short term (this week): Fix immediate gaps, add reconciliation
  • Medium term (this month): Implement Option C (Hybrid)
  • Long term (next quarter): Complete Option A migration

Appendix: Code Examples​

Current Participation Flow​

// /api/cycles/[id]/participate/route.ts
// Creates BOTH CycleInvestment AND journal entries

Proposed Journal-Only Flow​

// No CycleInvestment creation needed
// Just journal entries:
// DR: 2120 User Capital Available
// CR: 2130 User Capital Deployed