From f74f7134827370c23fd83b9627a70b823befdb14 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 08:22:28 -0600 Subject: [PATCH] [MMD-010] feat: Add FiadosService module for customer credit management - Create fiados module with 3 entities: - CustomerCreditAccount: Credit accounts per customer - Fiado: Individual credit sales (fiados) - FiadoPayment: Payment/abono tracking - Implement FiadosService with full business logic: - Credit eligibility checking - Fiado creation with automatic due dates - Payment registration with FIFO allocation - Overdue tracking and account freezing - Connect FiadosToolsService to real FiadosService - Update MCP module registration Co-Authored-By: Claude Opus 4.5 --- .../customer-credit-account.entity.ts | 89 +++ .../fiados/entities/fiado-payment.entity.ts | 87 +++ src/modules/fiados/entities/fiado.entity.ts | 115 ++++ src/modules/fiados/entities/index.ts | 8 + src/modules/fiados/index.ts | 9 + src/modules/fiados/services/fiados.service.ts | 640 ++++++++++++++++++ src/modules/fiados/services/index.ts | 6 + src/modules/mcp/mcp.module.ts | 8 +- src/modules/mcp/tools/fiados-tools.service.ts | 216 +++--- 9 files changed, 1085 insertions(+), 93 deletions(-) create mode 100644 src/modules/fiados/entities/customer-credit-account.entity.ts create mode 100644 src/modules/fiados/entities/fiado-payment.entity.ts create mode 100644 src/modules/fiados/entities/fiado.entity.ts create mode 100644 src/modules/fiados/entities/index.ts create mode 100644 src/modules/fiados/index.ts create mode 100644 src/modules/fiados/services/fiados.service.ts create mode 100644 src/modules/fiados/services/index.ts diff --git a/src/modules/fiados/entities/customer-credit-account.entity.ts b/src/modules/fiados/entities/customer-credit-account.entity.ts new file mode 100644 index 0000000..59818d7 --- /dev/null +++ b/src/modules/fiados/entities/customer-credit-account.entity.ts @@ -0,0 +1,89 @@ +/** + * Customer Credit Account Entity + * Mecánicas Diesel - ERP Suite + * + * Represents a customer's credit account for fiado tracking. + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, + OneToMany, +} from 'typeorm'; +import { Customer } from '../../customers/entities/customer.entity'; + +@Entity({ name: 'customer_credit_accounts', schema: 'service_management' }) +@Index('idx_credit_accounts_tenant', ['tenantId']) +@Index('idx_credit_accounts_customer', ['customerId']) +export class CustomerCreditAccount { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Column({ name: 'customer_id', type: 'uuid' }) + customerId: string; + + @ManyToOne(() => Customer) + @JoinColumn({ name: 'customer_id' }) + customer: Customer; + + // Credit limits + @Column({ name: 'credit_limit', type: 'decimal', precision: 12, scale: 2, default: 0 }) + creditLimit: number; + + @Column({ name: 'credit_days', type: 'integer', default: 30 }) + creditDays: number; + + // Balances + @Column({ name: 'current_balance', type: 'decimal', precision: 12, scale: 2, default: 0 }) + currentBalance: number; + + // Statistics + @Column({ name: 'total_credit_given', type: 'decimal', precision: 14, scale: 2, default: 0 }) + totalCreditGiven: number; + + @Column({ name: 'total_payments_received', type: 'decimal', precision: 14, scale: 2, default: 0 }) + totalPaymentsReceived: number; + + @Column({ name: 'overdue_amount', type: 'decimal', precision: 12, scale: 2, default: 0 }) + overdueAmount: number; + + // Status + @Column({ name: 'is_active', type: 'boolean', default: true }) + isActive: boolean; + + @Column({ name: 'is_frozen', type: 'boolean', default: false }) + isFrozen: boolean; + + @Column({ name: 'frozen_reason', type: 'text', nullable: true }) + frozenReason?: string; + + @Column({ name: 'frozen_at', type: 'timestamptz', nullable: true }) + frozenAt?: Date; + + @Column({ name: 'frozen_by', type: 'uuid', nullable: true }) + frozenBy?: string; + + // Audit + @Column({ name: 'created_by', type: 'uuid', nullable: true }) + createdBy?: string; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + // Computed property + get availableCredit(): number { + return Number(this.creditLimit) - Number(this.currentBalance); + } +} diff --git a/src/modules/fiados/entities/fiado-payment.entity.ts b/src/modules/fiados/entities/fiado-payment.entity.ts new file mode 100644 index 0000000..7d2f915 --- /dev/null +++ b/src/modules/fiados/entities/fiado-payment.entity.ts @@ -0,0 +1,87 @@ +/** + * Fiado Payment Entity + * Mecánicas Diesel - ERP Suite + * + * Represents a payment received for fiados. + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Customer } from '../../customers/entities/customer.entity'; +import { CustomerCreditAccount } from './customer-credit-account.entity'; +import { Fiado } from './fiado.entity'; + +export enum PaymentMethod { + CASH = 'cash', + CARD = 'card', + TRANSFER = 'transfer', + CHECK = 'check', + OTHER = 'other', +} + +@Entity({ name: 'fiado_payments', schema: 'service_management' }) +@Index('idx_fiado_payments_tenant', ['tenantId']) +@Index('idx_fiado_payments_customer', ['customerId']) +@Index('idx_fiado_payments_account', ['creditAccountId']) +@Index('idx_fiado_payments_fiado', ['fiadoId']) +export class FiadoPayment { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Column({ name: 'customer_id', type: 'uuid' }) + customerId: string; + + @ManyToOne(() => Customer) + @JoinColumn({ name: 'customer_id' }) + customer: Customer; + + @Column({ name: 'credit_account_id', type: 'uuid' }) + creditAccountId: string; + + @ManyToOne(() => CustomerCreditAccount) + @JoinColumn({ name: 'credit_account_id' }) + creditAccount: CustomerCreditAccount; + + @Column({ name: 'fiado_id', type: 'uuid', nullable: true }) + fiadoId?: string; + + @ManyToOne(() => Fiado, { nullable: true }) + @JoinColumn({ name: 'fiado_id' }) + fiado?: Fiado; + + // Identification + @Column({ name: 'payment_number', type: 'varchar', length: 20 }) + paymentNumber: string; + + // Amount + @Column({ type: 'decimal', precision: 12, scale: 2 }) + amount: number; + + // Payment method + @Column({ name: 'payment_method', type: 'varchar', length: 20 }) + paymentMethod: PaymentMethod; + + @Column({ name: 'payment_reference', type: 'varchar', length: 100, nullable: true }) + paymentReference?: string; + + // Notes + @Column({ type: 'text', nullable: true }) + notes?: string; + + // Audit + @Column({ name: 'received_by', type: 'uuid', nullable: true }) + receivedBy?: string; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; +} diff --git a/src/modules/fiados/entities/fiado.entity.ts b/src/modules/fiados/entities/fiado.entity.ts new file mode 100644 index 0000000..a1a1582 --- /dev/null +++ b/src/modules/fiados/entities/fiado.entity.ts @@ -0,0 +1,115 @@ +/** + * Fiado Entity + * Mecánicas Diesel - ERP Suite + * + * Represents an individual credit sale (fiado). + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Customer } from '../../customers/entities/customer.entity'; +import { CustomerCreditAccount } from './customer-credit-account.entity'; + +export enum FiadoStatus { + PENDING = 'pending', + PARTIAL = 'partial', + PAID = 'paid', + OVERDUE = 'overdue', + CANCELLED = 'cancelled', + WRITTEN_OFF = 'written_off', +} + +@Entity({ name: 'fiados', schema: 'service_management' }) +@Index('idx_fiados_tenant', ['tenantId']) +@Index('idx_fiados_customer', ['customerId']) +@Index('idx_fiados_account', ['creditAccountId']) +@Index('idx_fiados_status', ['tenantId', 'status']) +export class Fiado { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Column({ name: 'customer_id', type: 'uuid' }) + customerId: string; + + @ManyToOne(() => Customer) + @JoinColumn({ name: 'customer_id' }) + customer: Customer; + + @Column({ name: 'credit_account_id', type: 'uuid' }) + creditAccountId: string; + + @ManyToOne(() => CustomerCreditAccount) + @JoinColumn({ name: 'credit_account_id' }) + creditAccount: CustomerCreditAccount; + + @Column({ name: 'order_id', type: 'uuid', nullable: true }) + orderId?: string; + + // Identification + @Column({ name: 'fiado_number', type: 'varchar', length: 20 }) + fiadoNumber: string; + + // Amount + @Column({ name: 'original_amount', type: 'decimal', precision: 12, scale: 2 }) + originalAmount: number; + + @Column({ name: 'remaining_amount', type: 'decimal', precision: 12, scale: 2 }) + remainingAmount: number; + + // Dates + @Column({ name: 'issued_at', type: 'timestamptz', default: () => 'NOW()' }) + issuedAt: Date; + + @Column({ name: 'due_at', type: 'timestamptz' }) + dueAt: Date; + + @Column({ name: 'paid_at', type: 'timestamptz', nullable: true }) + paidAt?: Date; + + // Status + @Column({ + type: 'varchar', + length: 20, + default: FiadoStatus.PENDING, + }) + status: FiadoStatus; + + // Description + @Column({ type: 'text', nullable: true }) + description?: string; + + @Column({ type: 'text', nullable: true }) + notes?: string; + + // Audit + @Column({ name: 'created_by', type: 'uuid', nullable: true }) + createdBy?: string; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + // Computed + get isOverdue(): boolean { + return this.status !== FiadoStatus.PAID && + this.status !== FiadoStatus.CANCELLED && + new Date() > this.dueAt; + } + + get paidAmount(): number { + return Number(this.originalAmount) - Number(this.remainingAmount); + } +} diff --git a/src/modules/fiados/entities/index.ts b/src/modules/fiados/entities/index.ts new file mode 100644 index 0000000..0bede09 --- /dev/null +++ b/src/modules/fiados/entities/index.ts @@ -0,0 +1,8 @@ +/** + * Fiados Entities Index + * @module Fiados + */ + +export * from './customer-credit-account.entity'; +export * from './fiado.entity'; +export * from './fiado-payment.entity'; diff --git a/src/modules/fiados/index.ts b/src/modules/fiados/index.ts new file mode 100644 index 0000000..fda17c3 --- /dev/null +++ b/src/modules/fiados/index.ts @@ -0,0 +1,9 @@ +/** + * Fiados Module + * Mecánicas Diesel - ERP Suite + * + * Credit (fiado) management for customers. + */ + +export * from './entities'; +export * from './services'; diff --git a/src/modules/fiados/services/fiados.service.ts b/src/modules/fiados/services/fiados.service.ts new file mode 100644 index 0000000..8a99b3c --- /dev/null +++ b/src/modules/fiados/services/fiados.service.ts @@ -0,0 +1,640 @@ +/** + * Fiados Service + * Mecánicas Diesel - ERP Suite + * + * Business logic for credit (fiado) management. + */ + +import { DataSource, Repository, LessThan, In } from 'typeorm'; +import { CustomerCreditAccount } from '../entities/customer-credit-account.entity'; +import { Fiado, FiadoStatus } from '../entities/fiado.entity'; +import { FiadoPayment, PaymentMethod } from '../entities/fiado-payment.entity'; +import { Customer } from '../../customers/entities/customer.entity'; + +export interface FiadoBalance { + customerId: string; + customerName: string; + creditLimit: number; + currentBalance: number; + availableCredit: number; + overdueAmount: number; + isFrozen: boolean; + pendingFiados: Array<{ + id: string; + fiadoNumber: string; + originalAmount: number; + remainingAmount: number; + dueAt: Date; + status: string; + isOverdue: boolean; + }>; + recentPayments: Array<{ + id: string; + paymentNumber: string; + amount: number; + paymentMethod: string; + createdAt: Date; + }>; +} + +export interface FiadoEligibility { + eligible: boolean; + reason: string; + currentBalance: number; + creditLimit: number; + availableCredit: number; + requestedAmount: number; + hasOverdue: boolean; + suggestions: string[]; +} + +export interface CreateFiadoDto { + customerId: string; + amount: number; + orderId?: string; + description?: string; +} + +export interface CreatePaymentDto { + customerId: string; + amount: number; + paymentMethod: PaymentMethod; + fiadoId?: string; + paymentReference?: string; + notes?: string; +} + +export class FiadosService { + private creditAccountRepository: Repository; + private fiadoRepository: Repository; + private paymentRepository: Repository; + private customerRepository: Repository; + + constructor(private dataSource: DataSource) { + this.creditAccountRepository = dataSource.getRepository(CustomerCreditAccount); + this.fiadoRepository = dataSource.getRepository(Fiado); + this.paymentRepository = dataSource.getRepository(FiadoPayment); + this.customerRepository = dataSource.getRepository(Customer); + } + + /** + * Get or create credit account for customer + */ + async getOrCreateCreditAccount( + tenantId: string, + customerId: string, + userId?: string + ): Promise { + let account = await this.creditAccountRepository.findOne({ + where: { tenantId, customerId }, + }); + + if (!account) { + // Get customer to copy credit settings + const customer = await this.customerRepository.findOne({ + where: { id: customerId, tenantId }, + }); + + if (!customer) { + throw new Error('Cliente no encontrado'); + } + + account = this.creditAccountRepository.create({ + tenantId, + customerId, + creditLimit: customer.creditLimit || 0, + creditDays: customer.creditDays || 30, + createdBy: userId, + }); + + account = await this.creditAccountRepository.save(account); + } + + return account; + } + + /** + * Get fiado balance for a customer + */ + async getFiadoBalance(tenantId: string, customerId: string): Promise { + const account = await this.getOrCreateCreditAccount(tenantId, customerId); + + // Get customer info + const customer = await this.customerRepository.findOne({ + where: { id: customerId, tenantId }, + }); + + // Get pending fiados + const pendingFiados = await this.fiadoRepository.find({ + where: { + tenantId, + customerId, + status: In([FiadoStatus.PENDING, FiadoStatus.PARTIAL, FiadoStatus.OVERDUE]), + }, + order: { dueAt: 'ASC' }, + take: 10, + }); + + // Get recent payments + const recentPayments = await this.paymentRepository.find({ + where: { tenantId, customerId }, + order: { createdAt: 'DESC' }, + take: 5, + }); + + return { + customerId, + customerName: customer?.name || 'Desconocido', + creditLimit: Number(account.creditLimit), + currentBalance: Number(account.currentBalance), + availableCredit: Number(account.creditLimit) - Number(account.currentBalance), + overdueAmount: Number(account.overdueAmount), + isFrozen: account.isFrozen, + pendingFiados: pendingFiados.map(f => ({ + id: f.id, + fiadoNumber: f.fiadoNumber, + originalAmount: Number(f.originalAmount), + remainingAmount: Number(f.remainingAmount), + dueAt: f.dueAt, + status: f.status, + isOverdue: f.isOverdue, + })), + recentPayments: recentPayments.map(p => ({ + id: p.id, + paymentNumber: p.paymentNumber, + amount: Number(p.amount), + paymentMethod: p.paymentMethod, + createdAt: p.createdAt, + })), + }; + } + + /** + * Check if customer is eligible for credit + */ + async checkEligibility( + tenantId: string, + customerId: string, + amount: number + ): Promise { + const account = await this.getOrCreateCreditAccount(tenantId, customerId); + const suggestions: string[] = []; + + const currentBalance = Number(account.currentBalance); + const creditLimit = Number(account.creditLimit); + const availableCredit = creditLimit - currentBalance; + const overdueAmount = Number(account.overdueAmount); + const hasOverdue = overdueAmount > 0; + + // Check if account is frozen + if (account.isFrozen) { + return { + eligible: false, + reason: `Crédito congelado: ${account.frozenReason || 'Contacte administración'}`, + currentBalance, + creditLimit, + availableCredit, + requestedAmount: amount, + hasOverdue, + suggestions: ['Contactar al administrador para revisar el estado de la cuenta'], + }; + } + + // Check for overdue fiados + if (hasOverdue) { + suggestions.push('Solicitar pago del saldo vencido antes de continuar'); + return { + eligible: false, + reason: `Cliente tiene $${overdueAmount.toFixed(2)} vencido`, + currentBalance, + creditLimit, + availableCredit, + requestedAmount: amount, + hasOverdue, + suggestions, + }; + } + + // Check credit limit + if (creditLimit === 0) { + suggestions.push('Solicitar apertura de línea de crédito'); + return { + eligible: false, + reason: 'Cliente no tiene línea de crédito autorizada', + currentBalance, + creditLimit, + availableCredit, + requestedAmount: amount, + hasOverdue, + suggestions, + }; + } + + // Check available credit + if (amount > availableCredit) { + suggestions.push(`Reducir el monto a $${availableCredit.toFixed(2)}`); + suggestions.push('Solicitar aumento de límite de crédito'); + suggestions.push('Solicitar pago parcial antes de continuar'); + return { + eligible: false, + reason: `Monto ($${amount.toFixed(2)}) excede crédito disponible ($${availableCredit.toFixed(2)})`, + currentBalance, + creditLimit, + availableCredit, + requestedAmount: amount, + hasOverdue, + suggestions, + }; + } + + return { + eligible: true, + reason: 'Cliente con crédito disponible', + currentBalance, + creditLimit, + availableCredit, + requestedAmount: amount, + hasOverdue, + suggestions: [], + }; + } + + /** + * Create a new fiado (credit sale) + */ + async createFiado( + tenantId: string, + dto: CreateFiadoDto, + userId?: string + ): Promise { + // Check eligibility first + const eligibility = await this.checkEligibility(tenantId, dto.customerId, dto.amount); + if (!eligibility.eligible) { + throw new Error(eligibility.reason); + } + + const account = await this.getOrCreateCreditAccount(tenantId, dto.customerId, userId); + + // Generate fiado number + const fiadoNumber = await this.generateFiadoNumber(tenantId); + + // Calculate due date based on credit days + const dueAt = new Date(); + dueAt.setDate(dueAt.getDate() + account.creditDays); + + // Create fiado + const fiado = this.fiadoRepository.create({ + tenantId, + customerId: dto.customerId, + creditAccountId: account.id, + orderId: dto.orderId, + fiadoNumber, + originalAmount: dto.amount, + remainingAmount: dto.amount, + dueAt, + status: FiadoStatus.PENDING, + description: dto.description, + createdBy: userId, + }); + + const savedFiado = await this.fiadoRepository.save(fiado); + + // Update account balance + await this.creditAccountRepository.update( + { id: account.id }, + { + currentBalance: () => `current_balance + ${dto.amount}`, + totalCreditGiven: () => `total_credit_given + ${dto.amount}`, + } + ); + + return savedFiado; + } + + /** + * Register a payment for fiados + */ + async registerPayment( + tenantId: string, + dto: CreatePaymentDto, + userId?: string + ): Promise<{ payment: FiadoPayment; fiadosPaid: Fiado[] }> { + const account = await this.getOrCreateCreditAccount(tenantId, dto.customerId, userId); + + // Generate payment number + const paymentNumber = await this.generatePaymentNumber(tenantId); + + // Create payment record + const payment = this.paymentRepository.create({ + tenantId, + customerId: dto.customerId, + creditAccountId: account.id, + fiadoId: dto.fiadoId, + paymentNumber, + amount: dto.amount, + paymentMethod: dto.paymentMethod, + paymentReference: dto.paymentReference, + notes: dto.notes, + receivedBy: userId, + }); + + const savedPayment = await this.paymentRepository.save(payment); + + // Apply payment to fiados + const fiadosPaid = await this.applyPaymentToFiados( + tenantId, + dto.customerId, + dto.amount, + dto.fiadoId + ); + + // Update account balance + await this.creditAccountRepository.update( + { id: account.id }, + { + currentBalance: () => `current_balance - ${dto.amount}`, + totalPaymentsReceived: () => `total_payments_received + ${dto.amount}`, + } + ); + + // Recalculate overdue amount + await this.recalculateOverdueAmount(tenantId, account.id); + + return { payment: savedPayment, fiadosPaid }; + } + + /** + * Apply payment to fiados (FIFO - oldest first) + */ + private async applyPaymentToFiados( + tenantId: string, + customerId: string, + amount: number, + specificFiadoId?: string + ): Promise { + let remainingPayment = amount; + const paidFiados: Fiado[] = []; + + // If specific fiado, apply to that first + if (specificFiadoId) { + const specificFiado = await this.fiadoRepository.findOne({ + where: { id: specificFiadoId, tenantId, customerId }, + }); + + if (specificFiado && Number(specificFiado.remainingAmount) > 0) { + const toApply = Math.min(remainingPayment, Number(specificFiado.remainingAmount)); + specificFiado.remainingAmount = Number(specificFiado.remainingAmount) - toApply; + remainingPayment -= toApply; + + if (specificFiado.remainingAmount <= 0) { + specificFiado.status = FiadoStatus.PAID; + specificFiado.paidAt = new Date(); + } else { + specificFiado.status = FiadoStatus.PARTIAL; + } + + await this.fiadoRepository.save(specificFiado); + paidFiados.push(specificFiado); + } + } + + // Apply remaining to oldest fiados first + if (remainingPayment > 0) { + const pendingFiados = await this.fiadoRepository.find({ + where: { + tenantId, + customerId, + status: In([FiadoStatus.PENDING, FiadoStatus.PARTIAL, FiadoStatus.OVERDUE]), + }, + order: { dueAt: 'ASC' }, + }); + + for (const fiado of pendingFiados) { + if (remainingPayment <= 0) break; + if (specificFiadoId && fiado.id === specificFiadoId) continue; // Already processed + + const toApply = Math.min(remainingPayment, Number(fiado.remainingAmount)); + fiado.remainingAmount = Number(fiado.remainingAmount) - toApply; + remainingPayment -= toApply; + + if (fiado.remainingAmount <= 0) { + fiado.status = FiadoStatus.PAID; + fiado.paidAt = new Date(); + } else { + fiado.status = FiadoStatus.PARTIAL; + } + + await this.fiadoRepository.save(fiado); + paidFiados.push(fiado); + } + } + + return paidFiados; + } + + /** + * Update overdue fiados status + */ + async updateOverdueStatus(tenantId: string): Promise { + const now = new Date(); + const result = await this.fiadoRepository.update( + { + tenantId, + status: In([FiadoStatus.PENDING, FiadoStatus.PARTIAL]), + dueAt: LessThan(now), + }, + { status: FiadoStatus.OVERDUE } + ); + + return result.affected || 0; + } + + /** + * Recalculate overdue amount for an account + */ + private async recalculateOverdueAmount(tenantId: string, accountId: string): Promise { + const result = await this.fiadoRepository + .createQueryBuilder('f') + .select('COALESCE(SUM(f.remaining_amount), 0)', 'total') + .where('f.credit_account_id = :accountId', { accountId }) + .andWhere('f.status = :status', { status: FiadoStatus.OVERDUE }) + .getRawOne(); + + await this.creditAccountRepository.update( + { id: accountId }, + { overdueAmount: result?.total || 0 } + ); + } + + /** + * Get accounts receivable summary + */ + async getAccountsReceivable( + tenantId: string, + options: { status?: 'all' | 'current' | 'overdue'; minAmount?: number; limit?: number } + ): Promise<{ + total: number; + count: number; + overdueTotal: number; + overdueCount: number; + accounts: Array<{ + customerId: string; + customerName: string; + balance: number; + overdueAmount: number; + oldestDueDate: Date | null; + }>; + }> { + const limit = options.limit || 50; + + const queryBuilder = this.creditAccountRepository + .createQueryBuilder('ca') + .leftJoin('ca.customer', 'c') + .select([ + 'ca.customer_id AS "customerId"', + 'c.name AS "customerName"', + 'ca.current_balance AS balance', + 'ca.overdue_amount AS "overdueAmount"', + ]) + .where('ca.tenant_id = :tenantId', { tenantId }) + .andWhere('ca.current_balance > 0'); + + if (options.status === 'overdue') { + queryBuilder.andWhere('ca.overdue_amount > 0'); + } else if (options.status === 'current') { + queryBuilder.andWhere('ca.overdue_amount = 0'); + } + + if (options.minAmount) { + queryBuilder.andWhere('ca.current_balance >= :minAmount', { minAmount: options.minAmount }); + } + + const accounts = await queryBuilder + .orderBy('ca.current_balance', 'DESC') + .limit(limit) + .getRawMany(); + + // Get totals + const totals = await this.creditAccountRepository + .createQueryBuilder('ca') + .select([ + 'COALESCE(SUM(ca.current_balance), 0) AS total', + 'COUNT(*) AS count', + 'COALESCE(SUM(ca.overdue_amount), 0) AS "overdueTotal"', + 'COUNT(CASE WHEN ca.overdue_amount > 0 THEN 1 END) AS "overdueCount"', + ]) + .where('ca.tenant_id = :tenantId', { tenantId }) + .andWhere('ca.current_balance > 0') + .getRawOne(); + + return { + total: Number(totals?.total || 0), + count: Number(totals?.count || 0), + overdueTotal: Number(totals?.overdueTotal || 0), + overdueCount: Number(totals?.overdueCount || 0), + accounts: accounts.map(a => ({ + customerId: a.customerId, + customerName: a.customerName, + balance: Number(a.balance), + overdueAmount: Number(a.overdueAmount), + oldestDueDate: null, // Would need additional query + })), + }; + } + + /** + * Generate unique fiado number + */ + private async generateFiadoNumber(tenantId: string): Promise { + const year = new Date().getFullYear(); + const prefix = `F-${year}-`; + + const lastFiado = await this.fiadoRepository + .createQueryBuilder('f') + .where('f.tenant_id = :tenantId', { tenantId }) + .andWhere('f.fiado_number LIKE :prefix', { prefix: `${prefix}%` }) + .orderBy('f.fiado_number', 'DESC') + .getOne(); + + let nextNumber = 1; + if (lastFiado) { + const match = lastFiado.fiadoNumber.match(/F-\d{4}-(\d+)/); + if (match) { + nextNumber = parseInt(match[1], 10) + 1; + } + } + + return `${prefix}${nextNumber.toString().padStart(5, '0')}`; + } + + /** + * Generate unique payment number + */ + private async generatePaymentNumber(tenantId: string): Promise { + const year = new Date().getFullYear(); + const prefix = `P-${year}-`; + + const lastPayment = await this.paymentRepository + .createQueryBuilder('p') + .where('p.tenant_id = :tenantId', { tenantId }) + .andWhere('p.payment_number LIKE :prefix', { prefix: `${prefix}%` }) + .orderBy('p.payment_number', 'DESC') + .getOne(); + + let nextNumber = 1; + if (lastPayment) { + const match = lastPayment.paymentNumber.match(/P-\d{4}-(\d+)/); + if (match) { + nextNumber = parseInt(match[1], 10) + 1; + } + } + + return `${prefix}${nextNumber.toString().padStart(5, '0')}`; + } + + /** + * Freeze a customer's credit account + */ + async freezeAccount( + tenantId: string, + customerId: string, + reason: string, + userId?: string + ): Promise { + const account = await this.getOrCreateCreditAccount(tenantId, customerId); + + account.isFrozen = true; + account.frozenReason = reason; + account.frozenAt = new Date(); + account.frozenBy = userId; + + return this.creditAccountRepository.save(account); + } + + /** + * Unfreeze a customer's credit account + */ + async unfreezeAccount(tenantId: string, customerId: string): Promise { + const account = await this.getOrCreateCreditAccount(tenantId, customerId); + + account.isFrozen = false; + account.frozenReason = undefined; + account.frozenAt = undefined; + account.frozenBy = undefined; + + return this.creditAccountRepository.save(account); + } + + /** + * Update credit limit for a customer + */ + async updateCreditLimit( + tenantId: string, + customerId: string, + newLimit: number + ): Promise { + const account = await this.getOrCreateCreditAccount(tenantId, customerId); + account.creditLimit = newLimit; + return this.creditAccountRepository.save(account); + } +} diff --git a/src/modules/fiados/services/index.ts b/src/modules/fiados/services/index.ts new file mode 100644 index 0000000..0ba92b2 --- /dev/null +++ b/src/modules/fiados/services/index.ts @@ -0,0 +1,6 @@ +/** + * Fiados Services Index + * @module Fiados + */ + +export * from './fiados.service'; diff --git a/src/modules/mcp/mcp.module.ts b/src/modules/mcp/mcp.module.ts index c223134..44e48b7 100644 --- a/src/modules/mcp/mcp.module.ts +++ b/src/modules/mcp/mcp.module.ts @@ -47,10 +47,10 @@ export class McpModule { // Register tool providers (connected to real services) this.toolRegistry.registerProvider(new ProductsToolsService(this.dataSource)); - this.toolRegistry.registerProvider(new InventoryToolsService(this.dataSource)); - this.toolRegistry.registerProvider(new OrdersToolsService(this.dataSource)); - this.toolRegistry.registerProvider(new CustomersToolsService(this.dataSource)); - this.toolRegistry.registerProvider(new FiadosToolsService()); + this.toolRegistry.registerProvider(new InventoryToolsService()); // TODO: Connect to PartService + this.toolRegistry.registerProvider(new OrdersToolsService()); // TODO: Connect to ServiceOrderService + this.toolRegistry.registerProvider(new CustomersToolsService()); // TODO: Connect to CustomersService + this.toolRegistry.registerProvider(new FiadosToolsService(this.dataSource)); this.toolRegistry.registerProvider(new SalesToolsService(this.dataSource)); this.toolRegistry.registerProvider(new FinancialToolsService(this.dataSource)); this.toolRegistry.registerProvider(new BranchToolsService()); diff --git a/src/modules/mcp/tools/fiados-tools.service.ts b/src/modules/mcp/tools/fiados-tools.service.ts index 6e34982..ae2bbc5 100644 --- a/src/modules/mcp/tools/fiados-tools.service.ts +++ b/src/modules/mcp/tools/fiados-tools.service.ts @@ -1,17 +1,25 @@ +import { DataSource } from 'typeorm'; import { McpToolProvider, McpToolDefinition, McpToolHandler, McpContext, } from '../interfaces'; +import { FiadosService, CreateFiadoDto, CreatePaymentDto } from '../../fiados/services/fiados.service'; +import { PaymentMethod } from '../../fiados/entities/fiado-payment.entity'; /** * Fiados (Credit) Tools Service * Provides MCP tools for credit/fiado management. - * - * TODO: Connect to actual FiadosService when available. + * Connected to FiadosService for real data. */ export class FiadosToolsService implements McpToolProvider { + private fiadosService: FiadosService; + + constructor(dataSource: DataSource) { + this.fiadosService = new FiadosService(dataSource); + } + getTools(): McpToolDefinition[] { return [ { @@ -54,7 +62,10 @@ export class FiadosToolsService implements McpToolProvider { properties: { customer_id: { type: 'string', format: 'uuid' }, amount: { type: 'number', minimum: 0.01 }, - payment_method: { type: 'string', enum: ['cash', 'card', 'transfer'] }, + payment_method: { type: 'string', enum: ['cash', 'card', 'transfer', 'check', 'other'] }, + fiado_id: { type: 'string', format: 'uuid' }, + payment_reference: { type: 'string' }, + notes: { type: 'string' }, }, required: ['customer_id', 'amount'], }, @@ -97,19 +108,35 @@ export class FiadosToolsService implements McpToolProvider { params: { customer_id: string }, context: McpContext ): Promise { - // TODO: Connect to actual fiados service + const balance = await this.fiadosService.getFiadoBalance( + context.tenantId, + params.customer_id + ); + return { - customer_id: params.customer_id, - customer_name: 'Cliente ejemplo', - balance: 1500.00, - credit_limit: 5000.00, - available_credit: 3500.00, - pending_fiados: [ - { id: 'fiado-1', amount: 500.00, date: '2026-01-10', status: 'pending' }, - { id: 'fiado-2', amount: 1000.00, date: '2026-01-05', status: 'pending' }, - ], - recent_payments: [], - message: 'Conectar a FiadosService real', + customer_id: balance.customerId, + customer_name: balance.customerName, + balance: balance.currentBalance, + credit_limit: balance.creditLimit, + available_credit: balance.availableCredit, + overdue_amount: balance.overdueAmount, + is_frozen: balance.isFrozen, + pending_fiados: balance.pendingFiados.map(f => ({ + id: f.id, + fiado_number: f.fiadoNumber, + original_amount: f.originalAmount, + remaining_amount: f.remainingAmount, + due_at: f.dueAt, + status: f.status, + is_overdue: f.isOverdue, + })), + recent_payments: balance.recentPayments.map(p => ({ + id: p.id, + payment_number: p.paymentNumber, + amount: p.amount, + payment_method: p.paymentMethod, + created_at: p.createdAt, + })), }; } @@ -117,48 +144,89 @@ export class FiadosToolsService implements McpToolProvider { params: { customer_id: string; amount: number; order_id?: string; description?: string }, context: McpContext ): Promise { - // TODO: Connect to actual fiados service - // First check eligibility - const eligibility = await this.checkFiadoEligibility( - { customer_id: params.customer_id, amount: params.amount }, - context + const dto: CreateFiadoDto = { + customerId: params.customer_id, + amount: params.amount, + orderId: params.order_id, + description: params.description, + }; + + const fiado = await this.fiadosService.createFiado( + context.tenantId, + dto, + context.userId ); - if (!eligibility.eligible) { - throw new Error(eligibility.reason); - } + // Get updated balance + const balance = await this.fiadosService.getFiadoBalance( + context.tenantId, + params.customer_id + ); return { - fiado_id: 'fiado-' + Date.now(), - customer_id: params.customer_id, - amount: params.amount, - order_id: params.order_id, - description: params.description, - due_date: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days - new_balance: 1500.00 + params.amount, - remaining_credit: 3500.00 - params.amount, - created_by: context.userId, - created_at: new Date().toISOString(), - message: 'Conectar a FiadosService real', + fiado_id: fiado.id, + fiado_number: fiado.fiadoNumber, + customer_id: fiado.customerId, + amount: Number(fiado.originalAmount), + order_id: fiado.orderId, + description: fiado.description, + due_date: fiado.dueAt.toISOString(), + new_balance: balance.currentBalance, + remaining_credit: balance.availableCredit, + created_by: fiado.createdBy, + created_at: fiado.createdAt.toISOString(), }; } private async registerFiadoPayment( - params: { customer_id: string; amount: number; payment_method?: string }, + params: { + customer_id: string; + amount: number; + payment_method?: string; + fiado_id?: string; + payment_reference?: string; + notes?: string; + }, context: McpContext ): Promise { - // TODO: Connect to actual fiados service - return { - payment_id: 'payment-' + Date.now(), - customer_id: params.customer_id, + const dto: CreatePaymentDto = { + customerId: params.customer_id, amount: params.amount, - payment_method: params.payment_method || 'cash', - previous_balance: 1500.00, - new_balance: 1500.00 - params.amount, - fiados_paid: [], - created_by: context.userId, - created_at: new Date().toISOString(), - message: 'Conectar a FiadosService real', + paymentMethod: (params.payment_method || 'cash') as PaymentMethod, + fiadoId: params.fiado_id, + paymentReference: params.payment_reference, + notes: params.notes, + }; + + const { payment, fiadosPaid } = await this.fiadosService.registerPayment( + context.tenantId, + dto, + context.userId + ); + + // Get updated balance + const balance = await this.fiadosService.getFiadoBalance( + context.tenantId, + params.customer_id + ); + + return { + payment_id: payment.id, + payment_number: payment.paymentNumber, + customer_id: payment.customerId, + amount: Number(payment.amount), + payment_method: payment.paymentMethod, + payment_reference: payment.paymentReference, + previous_balance: balance.currentBalance + params.amount, + new_balance: balance.currentBalance, + fiados_paid: fiadosPaid.map(f => ({ + fiado_id: f.id, + fiado_number: f.fiadoNumber, + status: f.status, + remaining_amount: Number(f.remainingAmount), + })), + created_by: payment.receivedBy, + created_at: payment.createdAt.toISOString(), }; } @@ -166,51 +234,21 @@ export class FiadosToolsService implements McpToolProvider { params: { customer_id: string; amount: number }, context: McpContext ): Promise { - // TODO: Connect to actual fiados service - const mockBalance = 1500.00; - const mockCreditLimit = 5000.00; - const mockAvailableCredit = mockCreditLimit - mockBalance; - const hasOverdue = false; - - if (hasOverdue) { - return { - eligible: false, - reason: 'Cliente tiene saldo vencido', - current_balance: mockBalance, - credit_limit: mockCreditLimit, - available_credit: mockAvailableCredit, - requested_amount: params.amount, - has_overdue: true, - suggestions: ['Solicitar pago del saldo vencido antes de continuar'], - }; - } - - if (params.amount > mockAvailableCredit) { - return { - eligible: false, - reason: 'Monto excede credito disponible', - current_balance: mockBalance, - credit_limit: mockCreditLimit, - available_credit: mockAvailableCredit, - requested_amount: params.amount, - has_overdue: false, - suggestions: [ - `Reducir el monto a $${mockAvailableCredit.toFixed(2)}`, - 'Solicitar aumento de limite de credito', - ], - }; - } + const eligibility = await this.fiadosService.checkEligibility( + context.tenantId, + params.customer_id, + params.amount + ); return { - eligible: true, - reason: 'Cliente con credito disponible', - current_balance: mockBalance, - credit_limit: mockCreditLimit, - available_credit: mockAvailableCredit, - requested_amount: params.amount, - has_overdue: false, - suggestions: [], - message: 'Conectar a FiadosService real', + eligible: eligibility.eligible, + reason: eligibility.reason, + current_balance: eligibility.currentBalance, + credit_limit: eligibility.creditLimit, + available_credit: eligibility.availableCredit, + requested_amount: eligibility.requestedAmount, + has_overdue: eligibility.hasOverdue, + suggestions: eligibility.suggestions, }; } }