Розробка системи регуляторної звітності
Регуляторна звітність — це не одноразова задача при отриманні ліцензії, а постійне зобов'язання щодо дотримання. Регулятор очікує періодичних звітів, і пропуск терміну або помилка в даних визначає порушення ліцензії.
Типи регуляторної звітності
Періодичні звіти: квартальні та річні звіти регулятору про діяльність. Включають: обсяг транзакцій, кількість клієнтів, інциденти, статистику AML.
Звіти про підозрілу діяльність (SAR): при виявленні підозрілої діяльності. У більшості юрисдикцій — 15-30 днів з моменту виявлення.
Звіти про валютні транзакції (CTR): у США для транзакцій > $10,000 готівкою/крипто. Не всі юрисдикції вимагають еквівалент.
Повідомлення про інциденти: при серйозних інцидентах безпеки, порушеннях — повідомити регулятора протягом 4-72 годин.
Річний звіт про дотримання: річний звіт про стан програми AML, включаючи результати аудиту.
Архітектура системи звітності
interface RegulatoryReport {
id: string;
type: ReportType;
period: { from: Date; to: Date };
jurisdiction: string;
regulatorEmail: string;
dueDate: Date;
status: "DRAFT" | "REVIEW" | "SUBMITTED" | "ACKNOWLEDGED";
data: ReportData;
submittedAt?: Date;
acknowledgedAt?: Date;
submissionReference?: string;
}
class RegulatoryReportingService {
// Автоматичне створення періодичних звітів
async generatePeriodicReport(
type: "QUARTERLY" | "ANNUAL",
period: DateRange,
jurisdiction: string
): Promise<RegulatoryReport> {
const [
transactionStats,
customerStats,
amlStats,
incidentLog,
] = await Promise.all([
this.aggregateTransactionStats(period),
this.aggregateCustomerStats(period),
this.aggregateAMLStats(period),
this.getIncidents(period),
]);
const reportData = this.formatForJurisdiction(jurisdiction, {
period,
transactions: transactionStats,
customers: customerStats,
aml: amlStats,
incidents: incidentLog,
});
return this.db.createReport({
type,
period,
jurisdiction,
data: reportData,
dueDate: this.calculateDueDate(type, period),
status: "DRAFT",
});
}
private async aggregateTransactionStats(period: DateRange): Promise<TransactionStats> {
return this.db.query(`
SELECT
COUNT(*) as total_count,
SUM(usd_amount) as total_volume_usd,
AVG(usd_amount) as avg_transaction_usd,
COUNT(DISTINCT user_id) as unique_users,
asset,
direction
FROM transactions
WHERE created_at BETWEEN $1 AND $2
GROUP BY asset, direction
`, [period.from, period.to]);
}
private async aggregateAMLStats(period: DateRange): Promise<AMLStats> {
const [alerts, sars, blockedTransactions, kycStats] = await Promise.all([
this.db.countAlerts(period),
this.db.countSARs(period),
this.db.countBlockedTransactions(period),
this.db.getKYCStats(period),
]);
return {
totalAlerts: alerts.total,
alertsByLevel: alerts.byLevel,
sarsFiled: sars.count,
transactionsBlocked: blockedTransactions.count,
transactionsBlockedVolume: blockedTransactions.totalUSD,
kycApproved: kycStats.approved,
kycRejected: kycStats.rejected,
kycPending: kycStats.pending,
pepCustomers: kycStats.pepCount,
highRiskCustomers: kycStats.highRiskCount,
};
}
}
Автоматизоване формування SAR
class SARService {
async createSAR(alertId: string, context: SARContext): Promise<SAR> {
const alert = await this.db.getAlert(alertId);
const user = await this.db.getUser(alert.userId);
const transactions = await this.db.getAlertTransactions(alertId);
// Автоматично генеруємо наратив на основі даних сповіщення
const narrative = this.generateNarrative(alert, user, transactions);
const sar: SAR = {
id: crypto.randomUUID(),
alertId,
status: SARStatus.DRAFT,
filingInstitution: {
name: process.env.COMPANY_NAME,
vatNumber: process.env.COMPANY_VAT,
address: process.env.COMPANY_ADDRESS,
contactEmail: process.env.AML_OFFICER_EMAIL,
},
subject: {
name: `${user.firstName} ${user.lastName}`,
dob: user.dateOfBirth,
address: user.residenceAddress,
idNumber: user.documentNumber,
nationality: user.nationality,
},
suspiciousActivity: {
type: this.mapAlertTypeToSARCategory(alert.triggerRules),
dateRange: {
from: transactions[0]?.timestamp,
to: transactions[transactions.length - 1]?.timestamp,
},
totalAmount: transactions.reduce((sum, t) => sum + t.usdAmount, 0),
currency: "USD",
description: narrative,
},
supportingTransactions: transactions.map(t => ({
date: t.timestamp,
amount: t.amount,
asset: t.asset,
usdValue: t.usdAmount,
txHash: t.txHash,
counterpartyAddress: t.address,
})),
createdAt: new Date(),
dueDate: new Date(Date.now() + 15 * 86400000), // 15 днів
};
await this.db.saveSAR(sar);
return sar;
}
private generateNarrative(alert: Alert, user: User, transactions: Transaction[]): string {
const totalUSD = transactions.reduce((sum, t) => sum + t.usdAmount, 0);
const dateRange = `${formatDate(transactions[0].timestamp)} to ${formatDate(transactions[transactions.length-1].timestamp)}`;
return `
Customer ${user.firstName} ${user.lastName} (DOB: ${user.dateOfBirth},
nationality: ${user.nationality}) conducted suspicious activity between ${dateRange}.
Total value: USD ${totalUSD.toFixed(2)} across ${transactions.length} transactions.
Alert triggers: ${alert.triggerRules.join(", ")}.
${this.describePatterns(alert, transactions)}
Based on the foregoing, this activity is being reported as suspicious.
`.trim();
}
}
Управління терміннями
// Напоминання календаря для команди дотримання
const REPORTING_CALENDAR: ReportingSchedule[] = [
{ jurisdiction: "Estonia", type: "QUARTERLY", dueDays: 30, recipients: ["[email protected]"] },
{ jurisdiction: "Estonia", type: "ANNUAL", dueDays: 60, recipients: ["[email protected]", "[email protected]"] },
{ jurisdiction: "Lithuania", type: "QUARTERLY", dueDays: 30, recipients: ["[email protected]"] },
];
@Cron("0 9 * * *") // щодня о 9:00
async checkReportingDeadlines() {
const upcomingReports = await this.db.getUpcomingReports(30); // наступні 30 днів
for (const report of upcomingReports) {
const daysUntilDue = Math.ceil((report.dueDate - Date.now()) / 86400000);
if ([30, 14, 7, 3, 1].includes(daysUntilDue)) {
await this.sendDeadlineReminder(report, daysUntilDue);
}
}
}
Повна система регуляторної звітності з автоматичним генеруванням періодичних звітів, робочим процесом SAR та управлінням терміннями — 3-4 тижні розробки.







