@mazaventures/valentine
Version:
Toolkit for Venture Capital firms to run their back office efficiently and openly
166 lines (150 loc) • 5.2 kB
text/typescript
import { Company, CompanySchema, Investment, InvestmentSchema } from '../models/types';
import { z } from 'zod';
import Decimal from 'decimal.js';
/**
* Service for managing portfolio companies and investments.
* Handles company information, investment tracking, and ownership calculations.
*
* @example
* ```typescript
* const portfolio = new PortfolioService();
*
* // Add a company
* const company = await portfolio.addCompany({
* name: "TechCo",
* sector: "SaaS",
* stage: "SERIES_A",
* founded: new Date("2024-01-01")
* });
* ```
*/
export class PortfolioService {
private companies: Map<string, Company>;
private investments: Map<string, Investment>;
/**
* Initializes a new instance of the PortfolioService class.
*
* @description Creates a new PortfolioService instance with empty company and investment maps.
*/
constructor() {
this.companies = new Map();
this.investments = new Map();
}
/**
* Adds a new company to the portfolio.
*
* @param company - Company information without ID
* @returns Newly created company with generated ID
* @throws {Error} If company data validation fails
*/
async addCompany(company: Omit<Company, 'id'>): Promise<Company> {
const id = crypto.randomUUID();
const newCompany = CompanySchema.parse({ ...company, id });
this.companies.set(id, newCompany);
return newCompany;
}
/**
* Retrieves a company by its ID.
*
* @param id - Company ID
* @returns Company information or undefined if not found
*/
async getCompany(id: string): Promise<Company | undefined> {
return this.companies.get(id);
}
/**
* Updates an existing company's information.
*
* @param id - Company ID
* @param updates - Partial company information to update
* @returns Updated company information
* @throws {Error} If company not found or validation fails
*/
async updateCompany(id: string, updates: Partial<Omit<Company, 'id'>>): Promise<Company> {
const existing = this.companies.get(id);
if (!existing) {
throw new Error(`Company with id ${id} not found`);
}
const updated = CompanySchema.parse({ ...existing, ...updates });
this.companies.set(id, updated);
return updated;
}
/**
* Lists all companies in the portfolio.
*
* @returns Array of all companies
*/
async listCompanies(): Promise<Company[]> {
return Array.from(this.companies.values());
}
/**
* Records a new investment in a portfolio company.
*
* @param investment - Investment information without ID
* @returns Newly created investment with generated ID
* @throws {Error} If company not found or validation fails
*/
async addInvestment(investment: Omit<Investment, 'id'>): Promise<Investment> {
const id = crypto.randomUUID();
const newInvestment = InvestmentSchema.parse({ ...investment, id });
if (!this.companies.has(newInvestment.companyId)) {
throw new Error(`Company with id ${newInvestment.companyId} not found`);
}
this.investments.set(id, newInvestment);
return newInvestment;
}
/**
* Retrieves an investment by its ID.
*
* @param id - Investment ID
* @returns Investment information or undefined if not found
*/
async getInvestment(id: string): Promise<Investment | undefined> {
return this.investments.get(id);
}
/**
* Lists investments, optionally filtered by company.
*
* @param companyId - Optional company ID to filter investments
* @returns Array of investments
*/
async listInvestments(companyId?: string): Promise<Investment[]> {
const investments = Array.from(this.investments.values());
return companyId
? investments.filter(i => i.companyId === companyId)
: investments;
}
/**
* Calculates total invested amount, optionally for a specific company.
*
* @param companyId - Optional company ID to calculate total for
* @returns Object containing total amount and currency
*/
async getTotalInvested(companyId?: string): Promise<{ amount: Decimal; currency: string }> {
const investments = await this.listInvestments(companyId);
const byCurrency = new Map<string, Decimal>();
for (const inv of investments) {
const current = byCurrency.get(inv.currency) || new Decimal(0);
byCurrency.set(inv.currency, current.plus(inv.amount));
}
// For now, return the first currency group. In practice, you'd want to handle
// multiple currencies and possibly convert them to a base currency
const [firstCurrency] = byCurrency.entries();
return firstCurrency
? { amount: firstCurrency[1], currency: firstCurrency[0] }
: { amount: new Decimal(0), currency: 'USD' };
}
/**
* Calculates total ownership percentage in a company.
*
* @param companyId - Company ID to calculate ownership for
* @returns Total ownership percentage as a decimal
*/
async getOwnership(companyId: string): Promise<Decimal> {
const investments = await this.listInvestments(companyId);
return investments.reduce(
(total, inv) => total.plus(inv.ownership),
new Decimal(0)
);
}
}