All files / src/domains/finance/services banking.service.ts

0% Statements 0/45
0% Branches 0/30
0% Functions 0/8
0% Lines 0/42

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 };
  }
}