[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 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 08:22:28 -06:00
parent e26ab24aa5
commit f74f713482
9 changed files with 1085 additions and 93 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
/**
* Fiados Entities Index
* @module Fiados
*/
export * from './customer-credit-account.entity';
export * from './fiado.entity';
export * from './fiado-payment.entity';

View File

@ -0,0 +1,9 @@
/**
* Fiados Module
* Mecánicas Diesel - ERP Suite
*
* Credit (fiado) management for customers.
*/
export * from './entities';
export * from './services';

View File

@ -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<CustomerCreditAccount>;
private fiadoRepository: Repository<Fiado>;
private paymentRepository: Repository<FiadoPayment>;
private customerRepository: Repository<Customer>;
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<CustomerCreditAccount> {
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<FiadoBalance> {
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<FiadoEligibility> {
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<Fiado> {
// 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<Fiado[]> {
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<number> {
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<void> {
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<string> {
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<string> {
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<CustomerCreditAccount> {
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<CustomerCreditAccount> {
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<CustomerCreditAccount> {
const account = await this.getOrCreateCreditAccount(tenantId, customerId);
account.creditLimit = newLimit;
return this.creditAccountRepository.save(account);
}
}

View File

@ -0,0 +1,6 @@
/**
* Fiados Services Index
* @module Fiados
*/
export * from './fiados.service';

View File

@ -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());

View File

@ -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<any> {
// 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<any> {
// 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<any> {
// 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<any> {
// 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,
};
}
}