Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model, Types } from 'mongoose'; import { FinanceBankAccount, BankStatement, BankTransaction } from '../schemas/banking.schema'; import * as crypto from 'crypto'; @Injectable() export class BankingService { private readonly encryptionKey = crypto.scryptSync('be-vision-finance-secret-key-salt', 'salt', 32); private readonly iv = Buffer.alloc(16, 0); constructor( @InjectModel(FinanceBankAccount.name) private readonly bankAccountModel: Model<FinanceBankAccount>, @InjectModel(BankStatement.name) private readonly statementModel: Model<BankStatement>, @InjectModel(BankTransaction.name) private readonly transactionModel: Model<BankTransaction>, ) {} private encrypt(text: string): string { const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, this.iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); return encrypted; } private decrypt(encryptedText: string): string { const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, this.iv); let decrypted = decipher.update(encryptedText, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } async createBankAccount(tenantId: string, data: any): Promise<FinanceBankAccount> { const rawNumber = data.accountNumber; const encrypted = this.encrypt(rawNumber); const masked = rawNumber.slice(-4).padStart(rawNumber.length, '*'); const doc = new this.bankAccountModel({ ...data, tenantId: new Types.ObjectId(tenantId), accountNumberEncrypted: encrypted, accountNumberMasked: masked, ledgerAccountId: new Types.ObjectId(data.ledgerAccountId), legalEntityId: new Types.ObjectId(data.legalEntityId) }); return doc.save(); } async getDecryptedAccountNumber(tenantId: string, id: string): Promise<string> { const acc = await this.bankAccountModel.findOne({ _id: id, tenantId }).exec(); if (!acc) throw new NotFoundException('Bank Account not found'); return this.decrypt(acc.accountNumberEncrypted); } async listBankAccounts(tenantId: string): Promise<FinanceBankAccount[]> { return this.bankAccountModel.find({ tenantId }).exec(); } async importStatement(tenantId: string, bankAccountId: string, data: any): Promise<BankStatement> { const statement = new this.statementModel({ tenantId: new Types.ObjectId(tenantId), bankAccountId: new Types.ObjectId(bankAccountId), statementDate: data.statementDate || new Date(), startBalanceMinor: data.startBalanceMinor || 0, endBalanceMinor: data.endBalanceMinor || 0, status: 'draft' }); await statement.save(); if (data.transactions && Array.isArray(data.transactions)) { for (const tx of data.transactions) { await this.transactionModel.create({ tenantId: new Types.ObjectId(tenantId), bankAccountId: new Types.ObjectId(bankAccountId), bankStatementId: statement._id, transactionDate: tx.date || new Date(), amountMinor: tx.amountMinor, description: tx.description, reference: tx.reference, status: 'unmatched' }); } } return statement; } async autoMatchTransactions(tenantId: string, bankAccountId: string): Promise<{ matchedCount: number }> { const transactions = await this.transactionModel.find({ tenantId, bankAccountId, status: 'unmatched' }).exec(); let matchedCount = 0; // Simple auto-matching by reference or exact matching amount for (const tx of transactions) { // In real scenario, search for journal line matches if (tx.reference && tx.reference.startsWith('REF-')) { tx.status = 'matched'; await tx.save(); matchedCount++; } } return { matchedCount }; } } |