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