Lab Exercises
Hands-on practice with sample data. Build a fund from scratch, run a direct investment lifecycle, debug import errors, and test your understanding of everything from Modules 1-9.
Each exercise walks you through a realistic scenario. You can work through them as thought exercises (reading the data and predicting outcomes) or hands-on with a live Portfolio instance (building the XLSX files and importing them). The answer keys are hidden behind reveal buttons — try to work through each step before checking.
The exercises below use their own investor/asset names (INV-ALICE, FUND-HGF1, etc.) and a 4-investor $11M fund. The samples/ directory contains a separate scenario with different names (INV-001, FUND-001) and a 6-investor $17M fund. Both are self-contained — don't mix data between them.
To run these exercises against a live instance:
- Clone the
portfoliorepo and rundocker-compose up - Seed demo data:
php artisan db:seed --class=DatabaseSeederDemo - The repo includes a ready-made import file at
demo/Portfolio Bulk Import with data.xlsx— try uploading it to see the full pipeline in action - A Postman collection at
demo/Delio Portfolio.postman_collection.jsonhas example requests for every endpoint
You can also download blank import templates from the Data Import page in the UI (or via GET /api/bulk-import/template/{name}/download) and fill them in using the exercise data.
Walk through the complete lifecycle of a PE fund -- from creating investors to calculating performance metrics. You'll build the XLSX import step by step.
Step 1: Create Investors
Add these four investors to the Investors sheet:
| client_reference | first_name | last_name | |
|---|---|---|---|
| INV-ALICE | Alice | Smith | alice@example.com |
| INV-BOB | Bob | Johnson | bob@example.com |
| INV-CAROL | Carol | Williams | carol@example.com |
| INV-DAVID | David | Brown | david@example.com |
Step 2: Create the Fund Asset
Add this asset to the Assets sheet:
| Field | Value |
|---|---|
| client_reference | FUND-HGF1 |
| name | Helm Growth Fund I |
| class | equity |
| structure | private_equity_fund |
| currency | USD |
| subscription_type | drawdown |
| is_open | false |
After importing the asset, you must enable the INVESTOR_SUBSCRIPTION_DISTRIBUTION toggle via the UI or API before importing subscriptions. This is Gotcha #10 from Module 9.
Step 3: Create Subscriptions
Add these subscriptions to the Subscriptions sheet (total commitment: $11M):
| investor | asset | amount | date |
|---|---|---|---|
| INV-ALICE | FUND-HGF1 | 5000000 | 2025-01-15 |
| INV-BOB | FUND-HGF1 | 3000000 | 2025-01-15 |
| INV-CAROL | FUND-HGF1 | 2000000 | 2025-01-15 |
| INV-DAVID | FUND-HGF1 | 1000000 | 2025-01-15 |
Step 4: Issue a 25% Capital Call
First, define the investor group in the Capital Call Groups sheet:
| group_id | investor |
|---|---|
| GRP-CC1 | INV-ALICE |
| GRP-CC1 | INV-BOB |
| GRP-CC1 | INV-CAROL |
| GRP-CC1 | INV-DAVID |
Then add the capital call in the Capital Calls sheet:
| asset | group_id | percentage | date |
|---|---|---|---|
| FUND-HGF1 | GRP-CC1 | 25 | 2025-03-01 |
This calls 25% of each investor's commitment: Alice $1.25M, Bob $750K, Carol $500K, David $250K. Total called: $2.75M.
Step 5: Add a Q4 2025 Valuation
Add to the Valuations sheet:
| asset | investor | value | date |
|---|---|---|---|
| FUND-HGF1 | all | 12000000 | 2025-12-31 |
Using all for the investor creates a valuation record for every investor who holds units. The $12M total NAV is distributed pro-rata based on each investor's ownership percentage.
Step 6: Distribute Dividends
Add to the Distributions sheet (pro-rata $500K dividend):
| asset | investor | amount | type | date |
|---|---|---|---|---|
| FUND-HGF1 | INV-ALICE | 227272.73 | dividend | 2025-12-31 |
| FUND-HGF1 | INV-BOB | 136363.64 | dividend | 2025-12-31 |
| FUND-HGF1 | INV-CAROL | 90909.09 | dividend | 2025-12-31 |
| FUND-HGF1 | INV-DAVID | 45454.54 | dividend | 2025-12-31 |
Distribution amounts are pro-rata based on ownership: Alice 45.45%, Bob 27.27%, Carol 18.18%, David 9.09%.
Expected Outcomes
DPI = Total Distributions / Paid-In Capital = $500,000 / $2,750,000 = 0.1818x
RVPI = NAV / Paid-In Capital = $12,000,000 / $2,750,000 = 4.3636x
TVPI = (NAV + Total Distributions) / Paid-In Capital = ($12,000,000 + $500,000) / $2,750,000 = 4.5454x
Note: Paid-In Capital equals the total called amount ($2.75M), not the total committed amount ($11M). This is a common source of confusion. TVPI appears extremely high because only 25% of committed capital has been called, yet the fund is valued at $12M. The math is correct for the exercise -- the numbers are chosen for pedagogical clarity, not realism.
Sheet order in workbook: Investors → Assets → Subscriptions → Capital Call Groups → Capital Calls → Valuations → Distributions
Model a direct equity investment with unit-based pricing, multiple valuation points, and a dividend distribution.
Step 1: Create the Direct Asset
Add to the Direct - Assets sheet:
| Field | Value |
|---|---|
| client_reference | DIRECT-TECHCO |
| name | TechCo Series B |
| class | equity |
| structure | direct |
| currency | USD |
Step 2: Create Investments
Add to the Direct - Investments sheet (reusing investors from Exercise 1):
| investor | asset | amount | units | price_per_unit | date |
|---|---|---|---|---|---|
| INV-ALICE | DIRECT-TECHCO | 1000000 | 100000 | 10.00 | 2025-01-15 |
| INV-BOB | DIRECT-TECHCO | 500000 | 50000 | 10.00 | 2025-01-15 |
Step 3: Add Valuations
Add two valuation points to the Valuations sheet. For direct assets, valuations use value per unit (VPU):
| asset | investor | value_per_unit | date | Note |
|---|---|---|---|---|
| DIRECT-TECHCO | all | 15.00 | 2025-03-31 | Q1: 50% gain |
| DIRECT-TECHCO | all | 18.00 | 2025-06-30 | Q2: 80% gain from cost |
Step 4: Distribute Dividends
Add a $50K dividend (pro-rata by ownership: Alice 66.67%, Bob 33.33%):
| asset | investor | amount | type | date |
|---|---|---|---|---|
| DIRECT-TECHCO | INV-ALICE | 33333.33 | dividend | 2025-06-30 |
| DIRECT-TECHCO | INV-BOB | 16666.67 | dividend | 2025-06-30 |
Expected Outcomes
Alice: 100,000 units x $18/unit = $1,800,000 current value. Cost basis: $1,000,000. Unrealized gain: $800,000 (80% return on capital). Plus $33,333 dividend received.
Bob: 50,000 units x $18/unit = $900,000 current value. Cost basis: $500,000. Unrealized gain: $400,000 (80% return on capital). Plus $16,667 dividend received.
Key difference from PE Fund: Direct investments track units and VPU (value per unit), while PE fund assets use NAV. The calc service handles both models, but the underlying data structures differ. A direct investment's valuation is always VPU x units held.
Sheet order: Direct - Assets → Direct - Investments → Valuations → Distributions
The Shared sheet lets you mix transaction types in a single sheet. It auto-sorts by priority, but you need to understand the sorting to predict what happens.
Scenario
You have a Shared sheet with the following rows (assume investors and assets already exist):
| Row | transaction_type | investor | asset | amount | date |
|---|---|---|---|---|---|
| 1 | Distribution | INV-ALICE | FUND-HGF1 | 100000 | 2025-06-15 |
| 2 | Valuation | all | FUND-HGF1 | 8000000 | 2025-06-01 |
| 3 | Subscription | INV-ALICE | FUND-HGF1 | 2000000 | 2025-01-01 |
| 4 | Transfer | INV-ALICE | FUND-HGF1 | 500000 | 2025-03-01 |
| 5 | Valuation-Replace | all | FUND-HGF1 | 9000000 | 2025-06-01 |
Question: In what order will the Shared sheet process these rows?
The Shared sheet sorts by priority, then by settlement date within each priority:
Priority 0 (Subscriptions): Row 3 -- Subscription (2025-01-01)
Priority 1 (Transfers): Row 4 -- Transfer (2025-03-01)
Priority 2 (Valuations): Row 2 -- Valuation (2025-06-01), then Row 5 -- Valuation-Replace (2025-06-01)
Priority 4 (Distributions): Row 1 -- Distribution (2025-06-15)
Final order: 3 → 4 → 2 → 5 → 1
Note that the Valuation (Row 2) processes before the Valuation-Replace (Row 5), both on the same date. The Replace will overwrite the value from Row 2, so the final NAV for 2025-06-01 will be $9M, not $8M.
Also note: if this were NOT a Shared sheet (i.e., separate dedicated sheets), you'd need to manually arrange the sheets in this order. The Shared sheet handles the sorting automatically -- that's its key advantage.
Debug Challenge
Now consider this broken Shared sheet. What's wrong?
| Row | transaction_type | investor | asset | amount | date |
|---|---|---|---|---|---|
| 1 | Distribution | INV-ALICE | FUND-XYZ | 50000 | 2025-06-15 |
| 2 | Valuation | all | FUND-XYZ | 5000000 | 2025-06-01 |
Problem: There's no Subscription row in the Shared sheet, and this is for asset FUND-XYZ which presumably doesn't have existing subscriptions. Without a subscription, the investor doesn't hold units in the asset.
The Valuation (Row 2) will fail because the all investor keyword looks for investors who hold units -- but no one does. The Distribution (Row 1) will also fail because Alice doesn't hold units.
Even though the Shared sheet re-orders by priority (subscription first, then valuation, then distribution), there's no subscription row to process. The fix: add a Subscription row for INV-ALICE in FUND-XYZ.
Also note: the Shared sheet has saga compensation. If the Valuation succeeds but the Distribution fails, the Valuation is automatically reversed. In this case, both would fail.
For each error message below, identify the most likely cause and the fix. Click to reveal the answer.
Cause: The asset column in this row contains a client_reference that doesn't match any existing asset. Common reasons:
- Typo in the client_reference (e.g., "FUND-HGF1" vs "FUND_HGF1")
- The Assets sheet appears after this sheet in the workbook, so the asset hasn't been created yet
- The asset was soft-deleted (
deleted_atis not null)
Fix: Check the exact client_reference value. Verify the Assets sheet comes first in the workbook. Check for soft-deleted records in the database.
Cause: The investor column is empty or missing in row 12. This typically happens with non-pro-rata fund assets where each transaction must specify an investor. For pro-rata funds you can sometimes use all, but for non-pro-rata, an investor reference is always required.
Fix: Add the investor's client_reference to the investor column. If you intended to apply to all investors, check whether the asset is configured as pro-rata -- non-pro-rata assets don't support the all shorthand for this transaction type.
Cause: The asset referenced in row 3 doesn't have the INVESTOR_SUBSCRIPTION_DISTRIBUTION toggle enabled. This is Gotcha #10 from Module 9. The toggle is a prerequisite for importing subscriptions, capital calls, distributions, and cash pool transactions.
Fix: Go to the asset's configuration in the UI (Asset Settings) and enable the INVESTOR_SUBSCRIPTION_DISTRIBUTION toggle, or call the REST API to update the asset configuration. Then re-run the import.
Cause: You're using the Valuations sheet (additive only), and a valuation already exists for this asset + date + investor combination. This often happens when re-running an import after a partial failure, or when trying to correct a previously imported valuation.
Fix: Use the Valuations - Replace sheet instead, which will overwrite existing valuations for the same date. Or use the all - replace investor value on the regular Valuations sheet. Remember that the Replace sheet uses heading row 1 (not row 2), so adjust your row references accordingly.
Cause: You're trying to import a distribution or valuation for an investor who doesn't have a subscription (PE fund) or investment (Direct) in the asset. Units are created by subscriptions/investments -- without them, the investor has no position in the asset.
Common scenarios:
- The Subscriptions sheet appears after the Distributions sheet in the workbook (ordering problem)
- The investor's
client_referencedoesn't match the one used in the subscription - The subscription import failed silently (check import history)
Fix: Ensure subscriptions/investments are imported before distributions/valuations. Verify the investor holds units by checking the subscription import history or the investor's position in the UI.
Quick Reference Card
- Exercise 1: PE fund lifecycle -- investors, asset, subscriptions, capital call, valuation, distribution. Key metrics: TVPI ~4.55x, DPI ~0.18x.
- Exercise 2: Direct investment -- unit-based pricing, VPU valuations, dividend distribution. Key: track units and VPU, not NAV.
- Exercise 3: Shared sheet ordering -- priority 0 (subscription) through priority 4 (distribution). Auto-sorted, with saga compensation on failure.
- Exercise 4: Error diagnosis -- the five most common import errors and their root causes.
If you're working hands-on, check the samples/ directory for CSV files matching the data in these exercises. You can use them as starting points when building XLSX import files from the official templates (downloadable from the Data Import page in the UI).
Course Complete
You've made it through all 10 modules of Portfolio for Dummies. You now understand:
- The domain model -- assets, investors, transactions, and how they relate
- The 17 transaction types and when to use each one
- The XLSX import pipeline and its dependency chain
- The calculation engine and what TVPI, DPI, RVPI, and XIRR actually mean
- The three API surfaces and their coverage gaps
- Configuration, reporting, and the "My View" investor experience
- The honest strengths and weaknesses of the module
- The top 10 gotchas that trip up every new user
The most important thing you've learned is what Portfolio is not: it's not a general ledger, not a fund accounting system, and not a fully automated data pipeline. Knowing those boundaries is what separates a successful implementation from a painful one.