Initial commit
This commit is contained in:
206
src/core/accounting/accounting-standards.service.ts
Normal file
206
src/core/accounting/accounting-standards.service.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
// DBIS Accounting Standards Service
|
||||
// Fair value marking, commodity feed integration
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
|
||||
|
||||
export interface ValuationData {
|
||||
assetType: string;
|
||||
assetId: string;
|
||||
fairValue: number;
|
||||
currencyCode: string;
|
||||
valuationDate: Date;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export class AccountingStandardsService {
|
||||
/**
|
||||
* Get valuation rule for asset type
|
||||
*/
|
||||
async getValuationRule(assetType: string) {
|
||||
return await prisma.valuationRule.findFirst({
|
||||
where: {
|
||||
assetType,
|
||||
status: 'active',
|
||||
effectiveDate: {
|
||||
lte: new Date(),
|
||||
},
|
||||
OR: [
|
||||
{ expiryDate: null },
|
||||
{ expiryDate: { gte: new Date() } },
|
||||
],
|
||||
},
|
||||
orderBy: { effectiveDate: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark asset to fair value
|
||||
*/
|
||||
async markToFairValue(assetId: string, assetType: string, fairValue: number, currencyCode: string) {
|
||||
const rule = await this.getValuationRule(assetType);
|
||||
|
||||
if (!rule) {
|
||||
throw new Error(`No valuation rule found for asset type: ${assetType}`);
|
||||
}
|
||||
|
||||
// Update asset value based on type
|
||||
switch (assetType) {
|
||||
case 'commodity':
|
||||
await this.updateCommodityValue(assetId, fairValue);
|
||||
break;
|
||||
case 'security':
|
||||
await this.updateSecurityValue(assetId, fairValue);
|
||||
break;
|
||||
case 'fiat':
|
||||
case 'cbdc':
|
||||
// Fiat and CBDC are already at fair value
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported asset type for fair value marking: ${assetType}`);
|
||||
}
|
||||
|
||||
// Log valuation
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
eventType: 'valuation',
|
||||
entityType: assetType,
|
||||
entityId: assetId,
|
||||
action: 'mark_to_fair_value',
|
||||
details: {
|
||||
fairValue,
|
||||
currencyCode,
|
||||
valuationMethod: rule.valuationMethod,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update commodity value
|
||||
*/
|
||||
private async updateCommodityValue(commodityId: string, fairValue: number) {
|
||||
// In production, would update commodity price
|
||||
// For now, update commodity spot price if exists
|
||||
const commodity = await prisma.commodity.findFirst({
|
||||
where: {
|
||||
id: commodityId,
|
||||
},
|
||||
});
|
||||
|
||||
if (commodity) {
|
||||
await prisma.commodity.update({
|
||||
where: { id: commodityId },
|
||||
data: {
|
||||
spotPrice: new Decimal(fairValue),
|
||||
lastUpdated: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update security value
|
||||
*/
|
||||
private async updateSecurityValue(securityId: string, fairValue: number) {
|
||||
const security = await prisma.security.findFirst({
|
||||
where: {
|
||||
securityId,
|
||||
},
|
||||
});
|
||||
|
||||
if (security) {
|
||||
await prisma.security.update({
|
||||
where: { id: security.id },
|
||||
data: {
|
||||
price: new Decimal(fairValue),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commodity feed price
|
||||
*/
|
||||
async getCommodityFeedPrice(commodityType: string, unit: string): Promise<number | null> {
|
||||
const commodity = await prisma.commodity.findUnique({
|
||||
where: {
|
||||
commodityType_unit: {
|
||||
commodityType,
|
||||
unit,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!commodity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parseFloat(commodity.spotPrice.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get FX reference rate
|
||||
*/
|
||||
async getFXReferenceRate(baseCurrency: string, quoteCurrency: string): Promise<number | null> {
|
||||
const fxPair = await prisma.fxPair.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
baseCurrency,
|
||||
quoteCurrency,
|
||||
},
|
||||
{
|
||||
baseCurrency: quoteCurrency,
|
||||
quoteCurrency: baseCurrency,
|
||||
},
|
||||
],
|
||||
status: 'active',
|
||||
},
|
||||
include: {
|
||||
trades: {
|
||||
where: {
|
||||
status: 'settled',
|
||||
},
|
||||
orderBy: { timestampUtc: 'desc' },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!fxPair || fxPair.trades.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parseFloat(fxPair.trades[0].price.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create valuation rule
|
||||
*/
|
||||
async createValuationRule(
|
||||
assetType: string,
|
||||
valuationMethod: string,
|
||||
feedSource?: string,
|
||||
updateFrequency: string = 'real_time'
|
||||
) {
|
||||
return await prisma.valuationRule.create({
|
||||
data: {
|
||||
id: require('uuid').v4(),
|
||||
ruleId: require('uuid').v4(),
|
||||
assetType,
|
||||
valuationMethod,
|
||||
feedSource,
|
||||
updateFrequency,
|
||||
status: 'active',
|
||||
effectiveDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const accountingStandardsService = new AccountingStandardsService();
|
||||
|
||||
422
src/core/accounting/reporting-engine.service.ts
Normal file
422
src/core/accounting/reporting-engine.service.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
// DBIS Reporting Engine Service
|
||||
// Generate consolidated statements, SCB reports
|
||||
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { accountService } from '@/core/accounts/account.service';
|
||||
import { treasuryService } from '@/core/treasury/treasury.service';
|
||||
import prisma from '@/shared/database/prisma';
|
||||
|
||||
export interface ConsolidatedStatementData {
|
||||
statementType: string;
|
||||
periodStart: Date;
|
||||
periodEnd: Date;
|
||||
}
|
||||
|
||||
export interface SovereignReportData {
|
||||
sovereignBankId: string;
|
||||
reportType: string;
|
||||
reportPeriod: string;
|
||||
reportDate: Date;
|
||||
}
|
||||
|
||||
export class ReportingEngineService {
|
||||
/**
|
||||
* Generate Consolidated Sovereign Liquidity Report (CSLR)
|
||||
*/
|
||||
async generateCSLR(periodStart: Date, periodEnd: Date) {
|
||||
const banks = await prisma.sovereignBank.findMany({
|
||||
where: { status: 'active' },
|
||||
include: {
|
||||
liquidityPools: true,
|
||||
accounts: true,
|
||||
},
|
||||
});
|
||||
|
||||
const consolidatedData: Record<string, unknown> = {
|
||||
periodStart,
|
||||
periodEnd,
|
||||
reportDate: new Date(),
|
||||
totalBanks: banks.length,
|
||||
liquidityByCurrency: {},
|
||||
totalLiquidity: 0,
|
||||
bankDetails: [],
|
||||
};
|
||||
|
||||
for (const bank of banks) {
|
||||
const bankLiquidity = bank.liquidityPools.reduce(
|
||||
(sum, pool) => sum + parseFloat(pool.totalLiquidity.toString()),
|
||||
0
|
||||
);
|
||||
|
||||
const lcr = await treasuryService.calculateLCR(bank.id);
|
||||
const nsfr = await treasuryService.calculateNSFR(bank.id);
|
||||
|
||||
consolidatedData.bankDetails.push({
|
||||
sovereignBankId: bank.id,
|
||||
sovereignCode: bank.sovereignCode,
|
||||
name: bank.name,
|
||||
totalLiquidity: bankLiquidity,
|
||||
lcr,
|
||||
nsfr,
|
||||
});
|
||||
|
||||
// Aggregate by currency
|
||||
for (const pool of bank.liquidityPools) {
|
||||
const currency = pool.currencyCode;
|
||||
if (!consolidatedData.liquidityByCurrency[currency]) {
|
||||
consolidatedData.liquidityByCurrency[currency] = 0;
|
||||
}
|
||||
consolidatedData.liquidityByCurrency[currency] += parseFloat(pool.totalLiquidity.toString());
|
||||
}
|
||||
|
||||
consolidatedData.totalLiquidity += bankLiquidity;
|
||||
}
|
||||
|
||||
const statement = await prisma.consolidatedStatement.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
statementId: uuidv4(),
|
||||
statementType: 'CSLR',
|
||||
reportDate: new Date(),
|
||||
periodStart,
|
||||
periodEnd,
|
||||
status: 'final',
|
||||
statementData: consolidatedData,
|
||||
publishedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Cross-Border Settlement Exposures Report
|
||||
*/
|
||||
async generateCrossBorderExposureReport(periodStart: Date, periodEnd: Date) {
|
||||
const settlements = await prisma.ledgerEntry.findMany({
|
||||
where: {
|
||||
timestampUtc: {
|
||||
gte: periodStart,
|
||||
lte: periodEnd,
|
||||
},
|
||||
status: 'settled',
|
||||
},
|
||||
include: {
|
||||
debitAccount: {
|
||||
include: {
|
||||
sovereignBank: true,
|
||||
},
|
||||
},
|
||||
creditAccount: {
|
||||
include: {
|
||||
sovereignBank: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const exposures: Record<string, unknown> = {};
|
||||
const bankPairs: Record<string, number> = {};
|
||||
|
||||
for (const settlement of settlements) {
|
||||
const debitBank = settlement.debitAccount.sovereignBank.sovereignCode;
|
||||
const creditBank = settlement.creditAccount.sovereignBank.sovereignCode;
|
||||
|
||||
if (debitBank !== creditBank) {
|
||||
const pairKey = `${debitBank}_${creditBank}`;
|
||||
const amount = parseFloat(settlement.amount.toString());
|
||||
|
||||
if (!bankPairs[pairKey]) {
|
||||
bankPairs[pairKey] = 0;
|
||||
}
|
||||
bankPairs[pairKey] += amount;
|
||||
|
||||
// Track exposure by bank
|
||||
if (!exposures[debitBank]) {
|
||||
exposures[debitBank] = { outbound: 0, inbound: 0 };
|
||||
}
|
||||
if (!exposures[creditBank]) {
|
||||
exposures[creditBank] = { outbound: 0, inbound: 0 };
|
||||
}
|
||||
|
||||
exposures[debitBank].outbound += amount;
|
||||
exposures[creditBank].inbound += amount;
|
||||
}
|
||||
}
|
||||
|
||||
const reportData = {
|
||||
periodStart,
|
||||
periodEnd,
|
||||
reportDate: new Date(),
|
||||
totalCrossBorderSettlements: settlements.filter(
|
||||
(s) => s.debitAccount.sovereignBankId !== s.creditAccount.sovereignBankId
|
||||
).length,
|
||||
exposures,
|
||||
bankPairs,
|
||||
};
|
||||
|
||||
const statement = await prisma.consolidatedStatement.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
statementId: uuidv4(),
|
||||
statementType: 'CrossBorderExposure',
|
||||
reportDate: new Date(),
|
||||
periodStart,
|
||||
periodEnd,
|
||||
status: 'final',
|
||||
statementData: reportData,
|
||||
publishedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CBDC Reserve Adequacy Statement
|
||||
*/
|
||||
async generateCBDCReserveAdequacy(periodStart: Date, periodEnd: Date) {
|
||||
const cbdcIssuances = await prisma.cbdcIssuance.findMany({
|
||||
where: {
|
||||
timestampUtc: {
|
||||
gte: periodStart,
|
||||
lte: periodEnd,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
sovereignBank: true,
|
||||
},
|
||||
});
|
||||
|
||||
const adequacyData: Record<string, unknown> = {
|
||||
periodStart,
|
||||
periodEnd,
|
||||
reportDate: new Date(),
|
||||
totalIssuances: cbdcIssuances.length,
|
||||
bankAdequacy: [],
|
||||
totalCBDCIssued: 0,
|
||||
totalReserveBacking: 0,
|
||||
};
|
||||
|
||||
for (const issuance of cbdcIssuances) {
|
||||
const bankIssuances = cbdcIssuances.filter(
|
||||
(i) => i.sovereignBankId === issuance.sovereignBankId
|
||||
);
|
||||
|
||||
const totalIssued = bankIssuances.reduce(
|
||||
(sum, i) => sum + parseFloat(i.amountMinted.toString()),
|
||||
0
|
||||
);
|
||||
const totalBacking = bankIssuances.reduce(
|
||||
(sum, i) => sum + parseFloat(i.reserveBacking?.toString() || '0'),
|
||||
0
|
||||
);
|
||||
|
||||
adequacyData.bankAdequacy.push({
|
||||
sovereignBankId: issuance.sovereignBankId,
|
||||
sovereignCode: issuance.sovereignBank.sovereignCode,
|
||||
totalCBDCIssued: totalIssued,
|
||||
totalReserveBacking: totalBacking,
|
||||
adequacyRatio: totalBacking > 0 ? totalIssued / totalBacking : 0,
|
||||
});
|
||||
|
||||
adequacyData.totalCBDCIssued += totalIssued;
|
||||
adequacyData.totalReserveBacking += totalBacking;
|
||||
}
|
||||
|
||||
const statement = await prisma.consolidatedStatement.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
statementId: uuidv4(),
|
||||
statementType: 'CBDCReserveAdequacy',
|
||||
reportDate: new Date(),
|
||||
periodStart,
|
||||
periodEnd,
|
||||
status: 'final',
|
||||
statementData: adequacyData,
|
||||
publishedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SCB daily liquidity window report
|
||||
*/
|
||||
async generateDailyLiquidityReport(sovereignBankId: string, reportDate: Date) {
|
||||
const lcr = await treasuryService.calculateLCR(sovereignBankId);
|
||||
const nsfr = await treasuryService.calculateNSFR(sovereignBankId);
|
||||
const accounts = await accountService.getAccountsBySovereign(sovereignBankId);
|
||||
|
||||
const liquidityData = {
|
||||
reportDate,
|
||||
lcr,
|
||||
nsfr,
|
||||
totalAccounts: accounts.length,
|
||||
totalBalance: accounts.reduce((sum, acc) => sum + parseFloat(acc.balance), 0),
|
||||
availableBalance: accounts.reduce((sum, acc) => sum + parseFloat(acc.availableBalance), 0),
|
||||
reservedBalance: accounts.reduce((sum, acc) => sum + parseFloat(acc.reservedBalance), 0),
|
||||
};
|
||||
|
||||
const report = await prisma.sovereignReport.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
sovereignBankId,
|
||||
reportId: uuidv4(),
|
||||
reportType: 'daily_liquidity',
|
||||
reportPeriod: 'daily',
|
||||
reportDate,
|
||||
dueDate: new Date(reportDate.getTime() + 24 * 60 * 60 * 1000), // Next day
|
||||
status: 'submitted',
|
||||
reportData: liquidityData,
|
||||
submittedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SCB weekly FX reserve update
|
||||
*/
|
||||
async generateWeeklyFXReserveReport(sovereignBankId: string, reportDate: Date) {
|
||||
const accounts = await accountService.getAccountsBySovereign(sovereignBankId);
|
||||
const fxReserves: Record<string, number> = {};
|
||||
|
||||
for (const account of accounts) {
|
||||
if (account.assetType === 'fiat' || account.assetType === 'cbdc') {
|
||||
if (!fxReserves[account.currencyCode]) {
|
||||
fxReserves[account.currencyCode] = 0;
|
||||
}
|
||||
fxReserves[account.currencyCode] += parseFloat(account.balance);
|
||||
}
|
||||
}
|
||||
|
||||
const report = await prisma.sovereignReport.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
sovereignBankId,
|
||||
reportId: uuidv4(),
|
||||
reportType: 'weekly_fx_reserve',
|
||||
reportPeriod: 'weekly',
|
||||
reportDate,
|
||||
dueDate: new Date(reportDate.getTime() + 7 * 24 * 60 * 60 * 1000), // Next week
|
||||
status: 'submitted',
|
||||
reportData: {
|
||||
reportDate,
|
||||
fxReserves,
|
||||
totalReserves: Object.values(fxReserves).reduce((sum, val) => sum + val, 0),
|
||||
},
|
||||
submittedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SCB monthly AML compliance results
|
||||
*/
|
||||
async generateMonthlyAMLComplianceReport(sovereignBankId: string, reportDate: Date) {
|
||||
const monthStart = new Date(reportDate.getFullYear(), reportDate.getMonth(), 1);
|
||||
const monthEnd = new Date(reportDate.getFullYear(), reportDate.getMonth() + 1, 0);
|
||||
|
||||
const complianceRecords = await prisma.complianceRecord.findMany({
|
||||
where: {
|
||||
sovereignBankId,
|
||||
createdAt: {
|
||||
gte: monthStart,
|
||||
lte: monthEnd,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reportData = {
|
||||
reportDate,
|
||||
monthStart,
|
||||
monthEnd,
|
||||
totalChecks: complianceRecords.length,
|
||||
clearCount: complianceRecords.filter((r) => r.status === 'clear').length,
|
||||
flaggedCount: complianceRecords.filter((r) => r.status === 'flagged').length,
|
||||
blockedCount: complianceRecords.filter((r) => r.status === 'blocked').length,
|
||||
averageRiskScore: complianceRecords.length > 0
|
||||
? complianceRecords.reduce((sum, r) => sum + r.riskScore, 0) / complianceRecords.length
|
||||
: 0,
|
||||
};
|
||||
|
||||
const report = await prisma.sovereignReport.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
sovereignBankId,
|
||||
reportId: uuidv4(),
|
||||
reportType: 'monthly_aml_compliance',
|
||||
reportPeriod: 'monthly',
|
||||
reportDate,
|
||||
dueDate: new Date(reportDate.getFullYear(), reportDate.getMonth() + 1, 15), // 15th of next month
|
||||
status: 'submitted',
|
||||
reportData,
|
||||
submittedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SCB quarterly CBDC issuance audit
|
||||
*/
|
||||
async generateQuarterlyCBDCAudit(sovereignBankId: string, reportDate: Date) {
|
||||
const quarterStart = new Date(reportDate.getFullYear(), Math.floor(reportDate.getMonth() / 3) * 3, 1);
|
||||
const quarterEnd = new Date(reportDate.getFullYear(), Math.floor(reportDate.getMonth() / 3) * 3 + 3, 0);
|
||||
|
||||
const issuances = await prisma.cbdcIssuance.findMany({
|
||||
where: {
|
||||
sovereignBankId,
|
||||
timestampUtc: {
|
||||
gte: quarterStart,
|
||||
lte: quarterEnd,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reportData = {
|
||||
reportDate,
|
||||
quarterStart,
|
||||
quarterEnd,
|
||||
totalIssuances: issuances.length,
|
||||
totalMinted: issuances.reduce((sum, i) => sum + parseFloat(i.amountMinted.toString()), 0),
|
||||
totalBurned: issuances.reduce((sum, i) => sum + parseFloat(i.amountBurned.toString()), 0),
|
||||
netChange: issuances.reduce((sum, i) => sum + parseFloat(i.netChange.toString()), 0),
|
||||
issuances: issuances.map((i) => ({
|
||||
recordId: i.recordId,
|
||||
operationType: i.operationType,
|
||||
amountMinted: parseFloat(i.amountMinted.toString()),
|
||||
amountBurned: parseFloat(i.amountBurned.toString()),
|
||||
reserveBacking: i.reserveBacking ? parseFloat(i.reserveBacking.toString()) : null,
|
||||
timestampUtc: i.timestampUtc,
|
||||
})),
|
||||
};
|
||||
|
||||
const report = await prisma.sovereignReport.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
sovereignBankId,
|
||||
reportId: uuidv4(),
|
||||
reportType: 'quarterly_cbdc_audit',
|
||||
reportPeriod: 'quarterly',
|
||||
reportDate,
|
||||
dueDate: new Date(reportDate.getFullYear(), Math.floor(reportDate.getMonth() / 3) * 3 + 3, 15),
|
||||
status: 'submitted',
|
||||
reportData,
|
||||
submittedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
|
||||
export const reportingEngineService = new ReportingEngineService();
|
||||
|
||||
192
src/core/accounting/valuation.service.ts
Normal file
192
src/core/accounting/valuation.service.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
// Valuation Service
|
||||
// Real-time fair value calculation
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { accountingStandardsService } from './accounting-standards.service';
|
||||
|
||||
|
||||
export class ValuationService {
|
||||
/**
|
||||
* Calculate real-time fair value for an asset
|
||||
*/
|
||||
async calculateFairValue(assetType: string, assetId: string, currencyCode: string): Promise<number> {
|
||||
const rule = await accountingStandardsService.getValuationRule(assetType);
|
||||
|
||||
if (!rule) {
|
||||
throw new Error(`No valuation rule found for asset type: ${assetType}`);
|
||||
}
|
||||
|
||||
switch (rule.valuationMethod) {
|
||||
case 'fair_value':
|
||||
return await this.calculateFairValueDirect(assetType, assetId, currencyCode);
|
||||
case 'commodity_feed':
|
||||
return await this.calculateFromCommodityFeed(assetType, assetId, currencyCode);
|
||||
case 'fx_reference_rate':
|
||||
return await this.calculateFromFXRate(assetType, assetId, currencyCode);
|
||||
default:
|
||||
throw new Error(`Unsupported valuation method: ${rule.valuationMethod}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate fair value directly (for fiat, CBDC)
|
||||
*/
|
||||
private async calculateFairValueDirect(
|
||||
assetType: string,
|
||||
assetId: string,
|
||||
currencyCode: string
|
||||
): Promise<number> {
|
||||
if (assetType === 'fiat' || assetType === 'cbdc') {
|
||||
// Fiat and CBDC are already at fair value (1:1)
|
||||
const account = await prisma.bankAccount.findUnique({
|
||||
where: { id: assetId },
|
||||
});
|
||||
|
||||
if (account) {
|
||||
return parseFloat(account.balance.toString());
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Cannot calculate fair value directly for asset type: ${assetType}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate from commodity feed
|
||||
*/
|
||||
private async calculateFromCommodityFeed(
|
||||
assetType: string,
|
||||
assetId: string,
|
||||
currencyCode: string
|
||||
): Promise<number> {
|
||||
if (assetType !== 'commodity') {
|
||||
throw new Error('Commodity feed valuation only applies to commodities');
|
||||
}
|
||||
|
||||
// Get commodity
|
||||
const commodity = await prisma.commodity.findFirst({
|
||||
where: { id: assetId },
|
||||
});
|
||||
|
||||
if (!commodity) {
|
||||
throw new Error(`Commodity not found: ${assetId}`);
|
||||
}
|
||||
|
||||
// Get current price from feed
|
||||
const price = await accountingStandardsService.getCommodityFeedPrice(
|
||||
commodity.commodityType,
|
||||
commodity.unit
|
||||
);
|
||||
|
||||
if (!price) {
|
||||
throw new Error(`No price feed available for commodity: ${commodity.commodityType}`);
|
||||
}
|
||||
|
||||
// Get quantity from account or sub-ledger
|
||||
const account = await prisma.bankAccount.findFirst({
|
||||
where: {
|
||||
assetType: 'commodity',
|
||||
currencyCode: commodity.commodityType,
|
||||
},
|
||||
});
|
||||
|
||||
const quantity = account ? parseFloat(account.balance.toString()) : 0;
|
||||
|
||||
return price * quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate from FX reference rate
|
||||
*/
|
||||
private async calculateFromFXRate(
|
||||
assetType: string,
|
||||
assetId: string,
|
||||
currencyCode: string
|
||||
): Promise<number> {
|
||||
// Get account
|
||||
const account = await prisma.bankAccount.findUnique({
|
||||
where: { id: assetId },
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
throw new Error(`Account not found: ${assetId}`);
|
||||
}
|
||||
|
||||
const baseAmount = parseFloat(account.balance.toString());
|
||||
|
||||
// If account currency matches target currency, no conversion needed
|
||||
if (account.currencyCode === currencyCode) {
|
||||
return baseAmount;
|
||||
}
|
||||
|
||||
// Get FX rate
|
||||
const fxRate = await accountingStandardsService.getFXReferenceRate(
|
||||
account.currencyCode,
|
||||
currencyCode
|
||||
);
|
||||
|
||||
if (!fxRate) {
|
||||
throw new Error(
|
||||
`No FX rate available for ${account.currencyCode}/${currencyCode}`
|
||||
);
|
||||
}
|
||||
|
||||
return baseAmount * fxRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all assets to fair value (batch operation)
|
||||
*/
|
||||
async markAllToFairValue(sovereignBankId?: string) {
|
||||
const where: { assetType?: string; sovereignBankId?: string } = {};
|
||||
|
||||
if (sovereignBankId) {
|
||||
where.sovereignBankId = sovereignBankId;
|
||||
}
|
||||
|
||||
const accounts = await prisma.bankAccount.findMany({
|
||||
where,
|
||||
include: {
|
||||
sovereignBank: true,
|
||||
},
|
||||
});
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const account of accounts) {
|
||||
try {
|
||||
const fairValue = await this.calculateFairValue(
|
||||
account.assetType,
|
||||
account.id,
|
||||
account.currencyCode
|
||||
);
|
||||
|
||||
await accountingStandardsService.markToFairValue(
|
||||
account.id,
|
||||
account.assetType,
|
||||
fairValue,
|
||||
account.currencyCode
|
||||
);
|
||||
|
||||
results.push({
|
||||
accountId: account.id,
|
||||
assetType: account.assetType,
|
||||
fairValue,
|
||||
success: true,
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
accountId: account.id,
|
||||
assetType: account.assetType,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
export const valuationService = new ValuationService();
|
||||
|
||||
Reference in New Issue
Block a user