mirror of
https://github.com/luxfhe/research-ocp-fhe.git
synced 2026-11-03 06:05:38 +00:00
425 lines
13 KiB
TypeScript
425 lines
13 KiB
TypeScript
import redis, { KEY_PREFIXES, generateKey } from '../redis';
|
|
import {
|
|
Company,
|
|
CompanyCreateData,
|
|
RoundCreateData,
|
|
Investment,
|
|
Round,
|
|
} from '../types/company';
|
|
|
|
export class CompanyService {
|
|
/**
|
|
* 1. Create a new company
|
|
*/
|
|
async createCompany(companyData: CompanyCreateData): Promise<Company> {
|
|
try {
|
|
const companyId = companyData.contractAddress;
|
|
const now = new Date().toISOString();
|
|
|
|
const company: Company = {
|
|
id: companyId,
|
|
name: companyData.name,
|
|
founder: companyData.founder,
|
|
contractAddress: companyData.contractAddress,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
rounds: [],
|
|
investors: [],
|
|
};
|
|
|
|
// Store company data as individual fields
|
|
const companyKey = generateKey(KEY_PREFIXES.COMPANY, companyId);
|
|
await redis.hset(companyKey, {
|
|
id: company.id,
|
|
name: company.name,
|
|
founder: company.founder,
|
|
contractAddress: company.contractAddress,
|
|
createdAt: company.createdAt,
|
|
updatedAt: company.updatedAt,
|
|
rounds: JSON.stringify([]),
|
|
investors: JSON.stringify([]),
|
|
});
|
|
|
|
// Index by founder
|
|
const founderKey = generateKey(
|
|
KEY_PREFIXES.COMPANY_BY_FOUNDER,
|
|
companyData.founder
|
|
);
|
|
await redis.sadd(founderKey, companyId);
|
|
|
|
return company;
|
|
} catch (error) {
|
|
console.error('Error creating company:', error);
|
|
throw new Error('Failed to create company');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 2. Get all companies
|
|
*/
|
|
async getAllCompanies(): Promise<Company[]> {
|
|
try {
|
|
// Get all company keys
|
|
const companyKeys = await redis.keys(`${KEY_PREFIXES.COMPANY}:*`);
|
|
|
|
if (companyKeys.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
// Fetch all companies in parallel
|
|
const companies = await Promise.all(
|
|
companyKeys.map(async (key: string) => {
|
|
const companyId = key.split(':')[1];
|
|
console.log('companyId', companyId);
|
|
return await this.getCompanyById(companyId);
|
|
})
|
|
);
|
|
|
|
// Filter out null values and sort by creation date
|
|
return companies
|
|
.filter(
|
|
(company: Company | null): company is Company => company !== null
|
|
)
|
|
.sort(
|
|
(a: Company, b: Company) =>
|
|
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
);
|
|
} catch (error) {
|
|
console.error('Error fetching all companies:', error);
|
|
throw new Error('Failed to fetch companies');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 3. Get companies by founder address
|
|
*/
|
|
async getCompaniesByFounder(founderAddress: string): Promise<Company[]> {
|
|
try {
|
|
const founderKey = generateKey(
|
|
KEY_PREFIXES.COMPANY_BY_FOUNDER,
|
|
founderAddress
|
|
);
|
|
const companyIds = await redis.smembers(founderKey);
|
|
|
|
if (companyIds.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const companies = await Promise.all(
|
|
companyIds.map((id: string) => this.getCompanyById(id))
|
|
);
|
|
|
|
return companies.filter(
|
|
(company: Company | null): company is Company => company !== null
|
|
);
|
|
} catch (error) {
|
|
console.error('Error fetching companies by founder:', error);
|
|
throw new Error('Failed to fetch companies by founder');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 4. Get companies by investor address
|
|
*/
|
|
async getCompaniesByInvestor(investorAddress: string): Promise<Company[]> {
|
|
try {
|
|
const investorKey = generateKey(
|
|
KEY_PREFIXES.COMPANY_BY_INVESTOR,
|
|
investorAddress
|
|
);
|
|
const companyIds = await redis.smembers(investorKey);
|
|
|
|
if (companyIds.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const companies = await Promise.all(
|
|
companyIds.map((id: string) => this.getCompanyById(id))
|
|
);
|
|
|
|
return companies.filter(
|
|
(company: Company | null): company is Company => company !== null
|
|
);
|
|
} catch (error) {
|
|
console.error('Error fetching companies by investor:', error);
|
|
throw new Error('Failed to fetch companies by investor');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 5. Get company by name (slug)
|
|
*/
|
|
async getCompanyByName(companyName: string): Promise<Company | null> {
|
|
try {
|
|
// Get all companies and find by name
|
|
const companies = await this.getAllCompanies();
|
|
const company = companies.find(
|
|
c => c.name.toLowerCase() === companyName.toLowerCase()
|
|
);
|
|
return company || null;
|
|
} catch (error) {
|
|
console.error('Error fetching company by name:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 6. Get company by ID (helper method)
|
|
*/
|
|
async getCompanyById(companyId: string): Promise<Company | null> {
|
|
try {
|
|
const companyKey = generateKey(KEY_PREFIXES.COMPANY, companyId);
|
|
const companyData = await redis.hgetall(companyKey);
|
|
|
|
if (!companyData || Object.keys(companyData).length === 0) {
|
|
return null;
|
|
}
|
|
return {
|
|
id: companyId,
|
|
name: (companyData.name as string) || '',
|
|
founder: (companyData.founder as string) || '',
|
|
contractAddress: (companyData.contractAddress as string) || '',
|
|
createdAt: (companyData.createdAt as string) || '',
|
|
updatedAt: (companyData.updatedAt as string) || '',
|
|
rounds: (() => {
|
|
try {
|
|
if (Array.isArray(companyData.rounds)) {
|
|
// Convert any rounds with investors to investments structure
|
|
return companyData.rounds.map((round: Round) => {
|
|
if (round.investments) {
|
|
return round; // Already in new format
|
|
}
|
|
// If neither investments nor investors exist, initialize with empty array
|
|
return {
|
|
...round,
|
|
investments: [],
|
|
};
|
|
});
|
|
}
|
|
if (
|
|
typeof companyData.rounds === 'string' &&
|
|
companyData.rounds !== ''
|
|
) {
|
|
const parsedRounds = JSON.parse(companyData.rounds);
|
|
// Convert any rounds with investors to investments structure
|
|
return parsedRounds.map((round: Round) => {
|
|
if (round.investments) {
|
|
return round; // Already in new format
|
|
}
|
|
// If neither investments nor investors exist, initialize with empty array
|
|
return {
|
|
...round,
|
|
investments: [],
|
|
};
|
|
});
|
|
}
|
|
return [];
|
|
} catch (error) {
|
|
console.error('Error fetching company by ID:', error);
|
|
return [];
|
|
}
|
|
})(),
|
|
investors: (() => {
|
|
try {
|
|
if (Array.isArray(companyData.investors)) {
|
|
return companyData.investors;
|
|
}
|
|
if (
|
|
typeof companyData.investors === 'string' &&
|
|
companyData.investors !== ''
|
|
) {
|
|
return JSON.parse(companyData.investors);
|
|
}
|
|
return [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
})(),
|
|
};
|
|
} catch (error) {
|
|
console.error('Error fetching company by ID:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 7. Add a new round to a company
|
|
*/
|
|
async addRound(
|
|
companyId: string,
|
|
roundData: RoundCreateData
|
|
): Promise<Company> {
|
|
try {
|
|
const company = await this.getCompanyById(companyId);
|
|
if (!company) throw new Error('Company not found');
|
|
|
|
const companyKey = generateKey(KEY_PREFIXES.COMPANY, companyId);
|
|
|
|
const rounds = company.rounds || [];
|
|
const newRound = {
|
|
id: `${companyId}_round_${rounds.length + 1}`,
|
|
round_id: roundData.round_id,
|
|
type: roundData.type,
|
|
date: roundData.date,
|
|
investments: roundData.investments || [],
|
|
preMoneyValuation: roundData.preMoneyValuation,
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
|
|
rounds.push(newRound);
|
|
|
|
// Update company's global investors list (avoid duplicates)
|
|
const existingInvestors = company.investors || [];
|
|
const newInvestors = [...existingInvestors];
|
|
|
|
if (roundData.investments) {
|
|
// Store round-specific investment security IDs
|
|
const roundInvestmentsKey = generateKey(
|
|
KEY_PREFIXES.ROUND_INVESTMENTS,
|
|
newRound.id
|
|
);
|
|
const securityIds: string[] = [];
|
|
|
|
roundData.investments.forEach(investment => {
|
|
const exists = newInvestors.some(
|
|
existing => existing.address === investment.investor.address
|
|
);
|
|
if (!exists) {
|
|
newInvestors.push(investment.investor);
|
|
|
|
// Index company by investor for lookup
|
|
const investorKey = generateKey(
|
|
KEY_PREFIXES.COMPANY_BY_INVESTOR,
|
|
investment.investor.address
|
|
);
|
|
redis.sadd(investorKey, companyId).catch(console.error);
|
|
}
|
|
|
|
// Collect security IDs for this round
|
|
if (investment.investor.securityId) {
|
|
securityIds.push(investment.investor.securityId);
|
|
}
|
|
});
|
|
|
|
// Store security IDs for this round
|
|
if (securityIds.length > 0) {
|
|
redis.sadd(roundInvestmentsKey, securityIds).catch(console.error);
|
|
}
|
|
}
|
|
|
|
await redis.hset(companyKey, {
|
|
rounds: JSON.stringify(rounds),
|
|
investors: JSON.stringify(newInvestors),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
return (await this.getCompanyById(companyId)) as Company;
|
|
} catch (error) {
|
|
console.error('Error adding round:', error);
|
|
throw new Error('Failed to add round');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 8. Add investor to the last round (allows multiple investments from same investor)
|
|
*/
|
|
async addInvestorToLastRound(
|
|
companyId: string,
|
|
investor: { address: string; name?: string },
|
|
shareAmount: number,
|
|
sharePrice: number
|
|
): Promise<Company> {
|
|
try {
|
|
const company = await this.getCompanyById(companyId);
|
|
if (!company) throw new Error('Company not found');
|
|
|
|
if (company.rounds.length === 0) {
|
|
throw new Error('No rounds exist for this company');
|
|
}
|
|
|
|
company.rounds[company.rounds.length - 1].investments.push({
|
|
investor: investor,
|
|
shareAmount: shareAmount,
|
|
sharePrice: sharePrice,
|
|
});
|
|
|
|
company.investors.push(investor);
|
|
const companyKey = generateKey(KEY_PREFIXES.COMPANY, companyId);
|
|
company.updatedAt = new Date().toISOString();
|
|
await redis.hset(companyKey, {
|
|
rounds: JSON.stringify(company.rounds),
|
|
investors: JSON.stringify(company.investors),
|
|
updatedAt: company.updatedAt,
|
|
});
|
|
|
|
// Only index company by investor if not already indexed (unique lookup)
|
|
const investorKey = generateKey(
|
|
KEY_PREFIXES.COMPANY_BY_INVESTOR,
|
|
investor.address
|
|
);
|
|
await redis.sadd(investorKey, companyId); // Set automatically handles duplicates
|
|
|
|
return (await this.getCompanyById(companyId)) as Company;
|
|
} catch (error) {
|
|
console.error('Error adding investor to last round:', error);
|
|
throw new Error('Failed to add investor to last round');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 9. Get all security IDs for a specific round
|
|
*/
|
|
async getRoundSecurityIds(roundId: string): Promise<string[]> {
|
|
try {
|
|
const roundInvestmentsKey = generateKey(
|
|
KEY_PREFIXES.ROUND_INVESTMENTS,
|
|
roundId
|
|
);
|
|
const securityIds = await redis.smembers(roundInvestmentsKey);
|
|
return securityIds;
|
|
} catch (error) {
|
|
console.error('Error fetching round security IDs:', error);
|
|
throw new Error('Failed to fetch round security IDs');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 10. Update round visibility
|
|
*/
|
|
async updateRoundVisibility(
|
|
companyId: string,
|
|
roundId: string,
|
|
isPubliclyVisible: boolean
|
|
): Promise<Company> {
|
|
try {
|
|
const company = await this.getCompanyById(companyId);
|
|
if (!company) throw new Error('Company not found');
|
|
|
|
// Find and update the specific round
|
|
const roundIndex = company.rounds.findIndex(
|
|
round => round.id === roundId
|
|
);
|
|
if (roundIndex === -1) {
|
|
throw new Error('Round not found');
|
|
}
|
|
|
|
// Update the round's visibility
|
|
company.rounds[roundIndex].isPubliclyVisible = isPubliclyVisible;
|
|
|
|
// Save updated company data
|
|
const companyKey = generateKey(KEY_PREFIXES.COMPANY, companyId);
|
|
await redis.hset(companyKey, {
|
|
rounds: JSON.stringify(company.rounds),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
return (await this.getCompanyById(companyId)) as Company;
|
|
} catch (error) {
|
|
console.error('Error updating round visibility:', error);
|
|
throw new Error('Failed to update round visibility');
|
|
}
|
|
}
|
|
}
|
|
|
|
export const companyService = new CompanyService();
|