2026-10-16 13:28:55 -05:00
import { describe , expect , it } from 'vitest' ;
import {
EMPTY_MEANS ,
type OpenDoc ,
type PaymentDraft ,
applyAll ,
openDocsFilter ,
paymentPayload ,
paymentTotals ,
toOpenDoc ,
validatePayment ,
} from './payment-model' ;
import { PAYMENT_PATHS } from './payment-paths' ;
import { PAYMENT_SCREENS } from './payment-screens' ;
const incoming = PAYMENT_SCREENS . find (( s ) => s . kind === 'incoming' ) ! ;
const outgoing = PAYMENT_SCREENS . find (( s ) => s . kind === 'outgoing' ) ! ;
const doc = ( over : Partial < OpenDoc > = {}) : OpenDoc => ({
id : 'Invoices:206' ,
docEntry : 206 ,
docNum : 206 ,
kind : 'invoice' ,
type : 'it_Invoice' ,
label : 'Factura' ,
date : '2026-09-01' ,
dueDate : '2026-10-01' ,
reference : '' ,
total : 1000 ,
balance : 1000 ,
applied : 0 ,
... over ,
});
const draft = ( over : Partial < PaymentDraft > = {}) : PaymentDraft => ({
docType : 'rCustomer' ,
cardCode : 'C1' ,
docDate : '2026-10-16' ,
taxDate : '' ,
series : null ,
remarks : '' ,
journalRemarks : '' ,
means : { ... EMPTY_MEANS },
checks : [],
docs : [],
accounts : [],
... over ,
});
describe ( 'pantallas de pagos' , () => {
it ( 'son dos, con sus recursos y facturas de su lado' , () => {
expect ( PAYMENT_SCREENS . map (( s ) => [ s . path , s . resource , s . invoices . resource , s . credits . resource ])). toEqual ([
[ '/bancos/pagos-recibidos' , 'IncomingPayments' , 'Invoices' , 'CreditNotes' ],
[ '/bancos/pagos-efectuados' , 'VendorPayments' , 'PurchaseInvoices' , 'PurchaseCreditNotes' ],
]);
expect ( incoming . partner . docType ). toBe ( 'rCustomer' );
expect ( outgoing . partner . docType ). toBe ( 'rSupplier' );
});
it ( 'la lista de rutas del enrutador coincide con las pantallas' , () => {
expect ([... PAYMENT_PATHS ]. sort ()). toEqual ( PAYMENT_SCREENS . map (( s ) => s . path ). sort ());
});
it ( 'los tipos de objeto de SAP son 24 (pagos recibidos) y 46 (efectuados)' , () => {
expect ([ incoming . objectType , outgoing . objectType ]). toEqual ([ 24 , 46 ]);
});
});
describe ( 'documentos pendientes' , () => {
it ( 'el filtro pide los abiertos del socio en la moneda local, escapando las comillas' , () => {
expect ( openDocsFilter ( "O'N" , 'CLP' )). toBe ( `CardCode eq 'O''N' and DocumentStatus eq 'bost_Open' and DocCurrency eq 'CLP'` );
});
it ( 'una factura de SAP: el saldo es el total menos lo ya pagado, y el folio es la referencia' , () => {
const d = toOpenDoc ( incoming , 'invoice' , { DocEntry : 206 , DocNum : 206 , DocDate : '2026-09-01T00:00:00Z' , DocDueDate : '2026-10-01T00:00:00Z' , DocTotal : 7363125 , PaidToDate : 363125 , FolioPrefixString : 'F' , FolioNumber : 77 });
expect ( d ). toMatchObject ({ id : 'Invoices:206' , docEntry : 206 , kind : 'invoice' , type : 'it_Invoice' , date : '2026-09-01' , dueDate : '2026-10-01' , total : 7363125 , balance : 7000000 , applied : 0 , reference : 'F77' });
});
it ( 'una nota de crédito usa su propio recurso y tipo; sin folio, la referencia del documento' , () => {
const d = toOpenDoc ( incoming , 'credit' , { DocEntry : 16 , DocNum : 16 , DocTotal : 163625 , PaidToDate : 0 , NumAtCard : 'NC-1' });
expect ( d ). toMatchObject ({ id : 'CreditNotes:16' , kind : 'credit' , type : 'it_CredItnote' , label : 'Nota de crédito' , balance : 163625 , reference : 'NC-1' });
expect ( toOpenDoc ( outgoing , 'credit' , { DocEntry : 3 , DocNum : 3 , DocTotal : 1 , PaidToDate : 0 })). toMatchObject ({ type : 'it_PurchaseCreditNote' });
});
it ( '«pagar todo» aplica el saldo de cada documento, y quitarlo lo deja en cero' , () => {
const docs = [ doc ({ balance : 300 }), doc ({ id : 'x' , balance : 50.5 })];
expect ( applyAll ( docs , true ). map (( d ) => d . applied )). toEqual ([ 300 , 50.5 ]);
expect ( applyAll ( applyAll ( docs , true ), false ). map (( d ) => d . applied )). toEqual ([ 0 , 0 ]);
});
});
describe ( 'totales' , () => {
it ( 'suma facturas, descuenta notas de crédito y suma los medios de pago, incluidos los cheques' , () => {
const t = paymentTotals ({
means : { ... EMPTY_MEANS , cashSum : 100 , transferSum : 400 },
checks : [{ CheckSum : 250 }, { CheckSum : 50.5 }],
docs : [ doc ({ applied : 700 }), doc ({ id : 'b' , applied : 100 }), doc ({ id : 'c' , kind : 'credit' , applied : 150 })],
accounts : [],
});
expect ( t ). toMatchObject ({ invoices : 800 , credits : 150 , applied : 650 , cash : 100 , transfer : 400 , checks : 300.5 , means : 800.5 });
});
it ( 'lo que sobra de los medios sobre lo aplicado queda a cuenta; nunca negativo' , () => {
const base = { checks : [], accounts : [] };
expect ( paymentTotals ({ ... base , means : { ... EMPTY_MEANS , transferSum : 1000 }, docs : [ doc ({ applied : 600 })] }). onAccount ). toBe ( 400 );
expect ( paymentTotals ({ ... base , means : { ... EMPTY_MEANS , transferSum : 100 }, docs : [ doc ({ applied : 600 })] }). onAccount ). toBe ( 0 );
});
it ( 'suma las cuentas contables y redondea a dos decimales' , () => {
const t = paymentTotals ({ means : { ... EMPTY_MEANS , cashSum : 0.1 + 0.2 }, checks : [], docs : [], accounts : [{ SumPaid : 10.005 }, { SumPaid : 5 }] });
expect ( t . cash ). toBe ( 0.3 );
expect ( t . accounts ). toBe ( 15.01 );
});
});
describe ( 'validación' , () => {
const ok = () => draft ({ means : { ... EMPTY_MEANS , transferAccount : '1-1-010-20-000' , transferSum : 500 }, docs : [ doc ({ applied : 500 })] });
it ( 'un pago correcto no tiene errores' , () => expect ( validatePayment ( ok ())). toEqual ([]));
it ( 'exige el socio, la fecha y un medio de pago' , () => {
expect ( validatePayment ( draft ({ cardCode : '' , docDate : '' }))). toEqual ([ 'Indique el cliente.' , 'Indique la fecha de contabilización.' , 'Indique al menos un medio de pago con su importe.' ]);
expect ( validatePayment ( draft ({ docType : 'rSupplier' , cardCode : '' }))[ 0 ]). toBe ( 'Indique el proveedor.' );
});
it ( 'cada medio con importe exige su cuenta' , () => {
expect ( validatePayment ( draft ({ means : { ... EMPTY_MEANS , cashSum : 10 } }))). toEqual ([ 'Indique la cuenta del efectivo.' ]);
expect ( validatePayment ( draft ({ means : { ... EMPTY_MEANS , transferSum : 10 } }))). toEqual ([ 'Indique la cuenta de la transferencia.' ]);
});
it ( 'los cheques exigen importe, número, banco, vencimiento y la cuenta de cheques del pago' , () => {
const errors = validatePayment ( draft ({ checks : [{ CheckSum : 100 }] }));
expect ( errors ). toEqual ([ 'Indique la cuenta de cheques.' , 'Cheque 1: indique el número.' , 'Cheque 1: indique el banco.' , 'Cheque 1: indique el vencimiento.' ]);
const full = draft ({ means : { ... EMPTY_MEANS , checkAccount : '1-1-010-20-000' }, checks : [{ CheckSum : 100 , CheckNumber : 1 , BankCode : '001' , DueDate : '2026-11-30' }] });
expect ( validatePayment ( full )). toEqual ([]);
});
it ( 'una fila de cheque vacía se ignora' , () => {
expect ( validatePayment ({ ... ok (), checks : [{}] })). toEqual ([]);
});
it ( 'no deja aplicar más que el saldo de la factura (SAP lo recortaría en silencio)' , () => {
const d = draft ({ means : { ... EMPTY_MEANS , transferAccount : 'A' , transferSum : 9_999_999 }, docs : [ doc ({ balance : 327250 , applied : 9_999_999 })] });
expect ( validatePayment ( d )). toEqual ([ 'Factura 206: no se puede aplicar más que su saldo (327250).' ]);
});
it ( 'los medios de pago deben cubrir lo aplicado' , () => {
const d = draft ({ means : { ... EMPTY_MEANS , transferAccount : 'A' , transferSum : 100 }, docs : [ doc ({ applied : 600 })] });
expect ( validatePayment ( d )). toEqual ([ 'Los medios de pago (100) no cubren lo aplicado a los documentos (600).' ]);
});
it ( 'una nota de crédito aplicada reduce lo que los medios deben cubrir' , () => {
const d = draft ({ means : { ... EMPTY_MEANS , transferAccount : 'A' , transferSum : 450 }, docs : [ doc ({ applied : 600 }), doc ({ id : 'n' , kind : 'credit' , balance : 150 , applied : 150 })] });
expect ( validatePayment ( d )). toEqual ([]);
});
it ( 'un pago sin documentos queda a cuenta y es válido' , () => {
expect ( validatePayment ( draft ({ means : { ... EMPTY_MEANS , transferAccount : 'A' , transferSum : 50 } }))). toEqual ([]);
});
it ( 'a cuentas contables: exige cuentas con importe y que los medios igualen su total' , () => {
const base = { docType : 'rAccount' as const , cardCode : '' , means : { ... EMPTY_MEANS , cashAccount : 'C' , cashSum : 5000 } };
expect ( validatePayment ( draft ({ ... base , accounts : [] }))). toEqual ([ 'Agregue al menos una cuenta contable.' ]);
expect ( validatePayment ( draft ({ ... base , accounts : [{ AccountCode : '6-1-010-40-000' }] }))). toEqual ([ 'Cuenta 1: indique el importe.' , 'Los medios de pago (5000) deben igualar el total de las cuentas (0).' ]);
expect ( validatePayment ( draft ({ ... base , accounts : [{ AccountCode : '6-1-010-40-000' , SumPaid : 4000 }] }))). toEqual ([ 'Los medios de pago (5000) deben igualar el total de las cuentas (4000).' ]);
expect ( validatePayment ( draft ({ ... base , accounts : [{ AccountCode : '6-1-010-40-000' , SumPaid : 5000 }] }))). toEqual ([]);
});
});
describe ( 'cuerpo del pago' , () => {
it ( 'transferencia aplicada a una factura: solo lo indicado, con la fecha del pago si no hay otra' , () => {
const body = paymentPayload ( incoming , draft ({ means : { ... EMPTY_MEANS , transferAccount : '1-1-010-20-000' , transferSum : 100000 , transferReference : 'REF-1' }, docs : [ doc ({ docEntry : 819 , applied : 100000 })] }));
expect ( body ). toEqual ({
DocType : 'rCustomer' ,
CardCode : 'C1' ,
DocDate : '2026-10-16' ,
TransferAccount : '1-1-010-20-000' ,
TransferSum : 100000 ,
TransferDate : '2026-10-16' ,
TransferReference : 'REF-1' ,
PaymentInvoices : [{ DocEntry : 819 , InvoiceType : 'it_Invoice' , SumApplied : 100000 }],
});
});
it ( 'la nota de crédito se aplica con importe NEGATIVO y las facturas sin aplicar no se envían' , () => {
const body = paymentPayload ( incoming , draft ({
means : { ... EMPTY_MEANS , transferAccount : 'A' , transferSum : 163625 },
docs : [ doc ({ docEntry : 819 , applied : 327250 }), doc ({ id : 'CreditNotes:16' , docEntry : 16 , kind : 'credit' , type : 'it_CredItnote' , applied : 163625 }), doc ({ id : 'z' , docEntry : 5 , applied : 0 })],
}));
expect ( body [ 'PaymentInvoices' ]). toEqual ([
{ DocEntry : 819 , InvoiceType : 'it_Invoice' , SumApplied : 327250 },
{ DocEntry : 16 , InvoiceType : 'it_CredItnote' , SumApplied : - 163625 },
]);
});
it ( 'un pago a cuenta no lleva PaymentInvoices: la diferencia la deja a cuenta SAP' , () => {
const body = paymentPayload ( incoming , draft ({ means : { ... EMPTY_MEANS , transferAccount : 'A' , transferSum : 50000 } }));
expect ( body ). not . toHaveProperty ( 'PaymentInvoices' );
expect ( body [ 'TransferSum' ]). toBe ( 50000 );
});
it ( 'efectivo y transferencia a la vez; el medio sin importe no se envía' , () => {
const body = paymentPayload ( incoming , draft ({ means : { ... EMPTY_MEANS , cashAccount : 'C' , cashSum : 20 , transferAccount : 'T' , transferSum : null } }));
expect ( body ). toMatchObject ({ CashAccount : 'C' , CashSum : 20 });
for ( const k of [ 'TransferAccount' , 'TransferSum' , 'TransferDate' , 'CheckAccount' ]) expect ( body ). not . toHaveProperty ( k );
});
it ( 'los cheques llevan la cuenta de cheques en el pago, el país del banco y sin campos vacíos' , () => {
const body = paymentPayload ( incoming , draft ({
means : { ... EMPTY_MEANS , checkAccount : '1-1-010-20-000' },
checks : [{ CheckNumber : 8001 , BankCode : '001' , Branch : '' , AccounttNum : '00000001' , DueDate : '2026-11-30' , CheckSum : 30000 }, { CheckSum : 0 }],
}));
expect ( body [ 'CheckAccount' ]). toBe ( '1-1-010-20-000' );
expect ( body [ 'PaymentChecks' ]). toEqual ([{ CheckNumber : 8001 , BankCode : '001' , AccounttNum : '00000001' , DueDate : '2026-11-30' , CheckSum : 30000 , CountryCode : 'CL' }]);
});
it ( 'a cuentas contables: sin socio y con PaymentAccounts' , () => {
const body = paymentPayload ( outgoing , draft ({
docType : 'rAccount' ,
cardCode : 'ignorado' ,
means : { ... EMPTY_MEANS , cashAccount : '1-1-010-10-001' , cashSum : 5000 },
accounts : [{ AccountCode : '6-1-010-40-000' , SumPaid : 5000 , Decription : 'Gasto' }, { AccountCode : '' , SumPaid : 0 }],
}));
expect ( body ). toEqual ({
DocType : 'rAccount' ,
DocDate : '2026-10-16' ,
CashAccount : '1-1-010-10-001' ,
CashSum : 5000 ,
PaymentAccounts : [{ AccountCode : '6-1-010-40-000' , SumPaid : 5000 , Decription : 'Gasto' }],
});
});
it ( 'serie, comentarios y fecha del documento solo si se indicaron' , () => {
const body = paymentPayload ( outgoing , draft ({ docType : 'rSupplier' , series : 21 , taxDate : '2026-10-15' , remarks : 'Pago facturas' , journalRemarks : 'Pago' , means : { ... EMPTY_MEANS , transferAccount : 'T' , transferSum : 1 , transferDate : '2026-10-20' } }));
expect ( body ). toMatchObject ({ DocType : 'rSupplier' , Series : 21 , TaxDate : '2026-10-15' , Remarks : 'Pago facturas' , JournalRemarks : 'Pago' , TransferDate : '2026-10-20' });
const bare = paymentPayload ( outgoing , draft ({ docType : 'rSupplier' , means : { ... EMPTY_MEANS , transferAccount : 'T' , transferSum : 1 } }));
for ( const k of [ 'Series' , 'TaxDate' , 'Remarks' , 'JournalRemarks' ]) expect ( bare ). not . toHaveProperty ( k );
});
2026-10-16 14:26:42 -05:00
it ( 'las tarjetas de crédito suman a los medios, se validan y se envían con la moneda local' , () => {
const card = { CreditCard : 2 , CreditCardNumber : '1234' , CardValidUntil : '2030-12-31' , VoucherNum : 'V-1' , CreditSum : 1000.456 };
expect ( paymentTotals ({ means : { ... EMPTY_MEANS }, checks : [], cards : [ card ], docs : [], accounts : [] })). toMatchObject ({ cards : 1000.46 , means : 1000.46 , onAccount : 1000.46 });
expect ( validatePayment ( draft ({ cards : [ card ] }))). toEqual ([]);
expect ( validatePayment ( draft ({ cards : [{ ... card , CreditCardNumber : '12' , VoucherNum : '' }] }))). toEqual ([
'Tarjeta 1: indique los 4 últimos dígitos.' ,
'Tarjeta 1: indique el número de comprobante.' ,
]);
expect ( paymentPayload ( incoming , draft ({ cards : [ card , { CreditCard : '' , CreditSum : 0 }] }))[ 'PaymentCreditCards' ]). toEqual ([
{ CreditCard : 2 , CreditCardNumber : '1234' , CardValidUntil : '2030-12-31' , VoucherNum : 'V-1' , CreditSum : 1000.46 , CreditCur : 'CLP' },
]);
expect ( incoming . cards ). toBe ( true );
expect ( outgoing . cards ). toBe ( false );
});
2026-10-16 13:28:55 -05:00
});