@@ -102,6 +110,9 @@
| Efectivo | {{ fmt(totals().cash) }} |
| Transferencia | {{ fmt(totals().transfer) }} |
| Cheques | {{ fmt(totals().checks) }} |
+ @if (screen.cards) {
+
| Tarjetas de crédito | {{ fmt(totals().cards) }} |
+ }
| Total medios de pago | {{ fmt(totals().means) }} |
@if (!toAccounts()) {
| A cuenta del socio de negocio | {{ fmt(totals().onAccount) }} |
diff --git a/apps/web/src/app/features/banking/payment-form.page.ts b/apps/web/src/app/features/banking/payment-form.page.ts
index 116bf13..5e32948 100644
--- a/apps/web/src/app/features/banking/payment-form.page.ts
+++ b/apps/web/src/app/features/banking/payment-form.page.ts
@@ -25,6 +25,16 @@ const today = (): string => new Date().toISOString().slice(0, 10);
const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10) : '');
const money = (n: number): string => formatNumber(n, 2);
+const CREDIT_CARDS: VhLookup = {
+ title: 'Tarjetas de crédito',
+ resource: 'CreditCards',
+ orderby: 'CreditCardCode',
+ columns: [
+ { key: 'CreditCardCode', title: 'Código', width: '100px', mono: true },
+ { key: 'CreditCardName', title: 'Nombre' },
+ ],
+};
+
const BANKS: VhLookup = {
title: 'Bancos',
resource: 'Banks',
@@ -109,6 +119,7 @@ export class PaymentFormPage {
};
});
protected readonly checks = linkedSignal
(() => ((this.doc()?.['PaymentChecks'] as Row[] | undefined) ?? []).map((c) => ({ ...c, DueDate: dateOnly(c['DueDate']) })));
+ protected readonly cards = linkedSignal(() => ((this.doc()?.['PaymentCreditCards'] as Row[] | undefined) ?? []).map((c) => ({ ...c, CardValidUntil: dateOnly(c['CardValidUntil']) })));
protected readonly accounts = linkedSignal(() => ((this.doc()?.['PaymentAccounts'] as Row[] | undefined) ?? []).map((a) => ({ ...a })));
protected readonly docs = linkedSignal(() =>
((this.doc()?.['PaymentInvoices'] as Row[] | undefined) ?? []).map((i) => ({
@@ -137,7 +148,7 @@ export class PaymentFormPage {
})(),
);
- protected readonly totals = computed(() => paymentTotals({ means: this.means(), checks: this.checks(), docs: this.docs(), accounts: this.accounts() }));
+ protected readonly totals = computed(() => paymentTotals({ means: this.means(), checks: this.checks(), cards: this.cards(), docs: this.docs(), accounts: this.accounts() }));
protected readonly toAccounts = computed(() => this.docType() === 'rAccount');
protected readonly fmt = money;
@@ -199,6 +210,13 @@ export class PaymentFormPage {
{ key: 'DueDate', title: 'Vencimiento', type: 'date', required: true, width: '130px' },
{ key: 'CheckSum', title: 'Importe', type: 'number', decimals: 2, required: true, width: '130px' },
];
+ protected readonly cardColumns: readonly DetailColumn[] = [
+ { key: 'CreditCard', title: 'Tarjeta', type: 'lookup', required: true, lookup: CREDIT_CARDS, width: '90px' },
+ { key: 'CreditCardNumber', title: '4 últimos dígitos', type: 'text', required: true, width: '130px' },
+ { key: 'CardValidUntil', title: 'Válida hasta', type: 'date', required: true, width: '130px' },
+ { key: 'VoucherNum', title: 'Comprobante', type: 'text', required: true, width: '130px' },
+ { key: 'CreditSum', title: 'Importe', type: 'number', decimals: 2, required: true, width: '130px' },
+ ];
protected readonly accountColumns: readonly DetailColumn[] = [
{ key: 'AccountCode', title: 'Cuenta', type: 'lookup', required: true, lookup: ACCOUNT_LOOKUP, width: '170px' },
{ key: 'Decription', title: 'Descripción', type: 'text', width: '260px' },
@@ -275,6 +293,15 @@ export class PaymentFormPage {
this.message.set(null);
}
+ protected setCards(rows: Row[]): void {
+ this.cards.set(rows);
+ this.message.set(null);
+ }
+
+ protected addCard(): void {
+ this.setCards([...this.cards(), { CardValidUntil: '' }]);
+ }
+
protected setAccounts(rows: Row[]): void {
this.accounts.set(rows);
this.message.set(null);
@@ -327,6 +354,7 @@ export class PaymentFormPage {
journalRemarks: this.journalRemarks(),
means: this.means(),
checks: this.checks(),
+ cards: this.cards(),
docs: this.docs(),
accounts: this.accounts(),
};
diff --git a/apps/web/src/app/features/banking/payment-model.spec.ts b/apps/web/src/app/features/banking/payment-model.spec.ts
index 223c90c..9bc0d69 100644
--- a/apps/web/src/app/features/banking/payment-model.spec.ts
+++ b/apps/web/src/app/features/banking/payment-model.spec.ts
@@ -236,4 +236,19 @@ describe('cuerpo del pago', () => {
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);
});
+
+ 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);
+ });
});
diff --git a/apps/web/src/app/features/banking/payment-model.ts b/apps/web/src/app/features/banking/payment-model.ts
index 644f3c4..a7330af 100644
--- a/apps/web/src/app/features/banking/payment-model.ts
+++ b/apps/web/src/app/features/banking/payment-model.ts
@@ -64,6 +64,8 @@ export interface PaymentDraft {
journalRemarks: string;
means: Means;
checks: Row[];
+ /** Tarjetas de crédito (solo pagos recibidos). */
+ cards?: Row[];
docs: OpenDoc[];
accounts: Row[];
}
@@ -113,6 +115,7 @@ export interface PaymentTotals {
cash: number;
transfer: number;
checks: number;
+ cards: number;
/** Suma de los medios de pago. */
means: number;
/** Lo que sobra de los medios sobre lo aplicado: queda a cuenta del socio. */
@@ -121,13 +124,14 @@ export interface PaymentTotals {
accounts: number;
}
-export function paymentTotals(d: Pick): PaymentTotals {
+export function paymentTotals(d: Pick & { cards?: Row[] }): PaymentTotals {
const invoices = round2(d.docs.filter((x) => x.kind === 'invoice').reduce((s, x) => s + num(x.applied), 0));
const credits = round2(d.docs.filter((x) => x.kind === 'credit').reduce((s, x) => s + num(x.applied), 0));
const cash = round2(num(d.means.cashSum));
const transfer = round2(num(d.means.transferSum));
const checks = round2(d.checks.reduce((s, c) => s + num(c['CheckSum']), 0));
- const means = round2(cash + transfer + checks);
+ const cards = round2((d.cards ?? []).reduce((s, c) => s + num(c['CreditSum']), 0));
+ const means = round2(cash + transfer + checks + cards);
const applied = round2(invoices - credits);
return {
invoices,
@@ -136,6 +140,7 @@ export function paymentTotals(d: Pick s + num(a['SumPaid']), 0)),
@@ -169,6 +174,15 @@ export function validatePayment(draft: PaymentDraft): string[] {
if (!has(c['DueDate'])) errors.push(`Cheque ${i + 1}: indique el vencimiento.`);
});
+ (draft.cards ?? []).forEach((c, i) => {
+ if (!has(c['CreditSum']) && !has(c['CreditCard']) && !has(c['CreditCardNumber'])) return; // fila vacía
+ if (num(c['CreditSum']) <= 0) errors.push(`Tarjeta ${i + 1}: indique el importe.`);
+ if (!has(c['CreditCard'])) errors.push(`Tarjeta ${i + 1}: indique la tarjeta.`);
+ if (!/^\d{4}$/.test(String(c['CreditCardNumber'] ?? '').trim())) errors.push(`Tarjeta ${i + 1}: indique los 4 últimos dígitos.`);
+ if (!has(c['CardValidUntil'])) errors.push(`Tarjeta ${i + 1}: indique la fecha de validez.`);
+ if (!has(c['VoucherNum'])) errors.push(`Tarjeta ${i + 1}: indique el número de comprobante.`);
+ });
+
if (toAccounts) {
const rows = draft.accounts.filter((a) => has(a['AccountCode']) || has(a['SumPaid']));
if (!rows.length) errors.push('Agregue al menos una cuenta contable.');
@@ -226,6 +240,20 @@ export function paymentPayload(screen: PaymentScreen, d: PaymentDraft): Row {
);
}
+ const cards = (d.cards ?? []).filter((c) => num(c['CreditSum']) > 0);
+ if (cards.length) {
+ body['PaymentCreditCards'] = cards.map((c) =>
+ clean({
+ CreditCard: c['CreditCard'],
+ CreditCardNumber: String(c['CreditCardNumber']).trim(),
+ CardValidUntil: c['CardValidUntil'],
+ VoucherNum: c['VoucherNum'],
+ CreditSum: round2(num(c['CreditSum'])),
+ CreditCur: screen.localCurrency,
+ }),
+ );
+ }
+
if (d.docType === 'rAccount') {
body['PaymentAccounts'] = d.accounts.filter((a) => num(a['SumPaid']) > 0).map((a) => clean({ AccountCode: a['AccountCode'], SumPaid: round2(num(a['SumPaid'])), Decription: a['Decription'] }));
} else {
diff --git a/apps/web/src/app/features/banking/payment-screens.ts b/apps/web/src/app/features/banking/payment-screens.ts
index f9c1898..154b9eb 100644
--- a/apps/web/src/app/features/banking/payment-screens.ts
+++ b/apps/web/src/app/features/banking/payment-screens.ts
@@ -50,6 +50,7 @@ export const PAYMENT_SCREENS: readonly PaymentScreen[] = [
objectType: 24,
localCurrency: 'CLP',
countryCode: 'CL',
+ cards: true,
columns: columns('Cliente'),
filters: [statusFilter, dateFilter],
orderby: 'DocEntry desc',
@@ -66,6 +67,7 @@ export const PAYMENT_SCREENS: readonly PaymentScreen[] = [
objectType: 46,
localCurrency: 'CLP',
countryCode: 'CL',
+ cards: false,
columns: columns('Proveedor'),
filters: [statusFilter, dateFilter],
orderby: 'DocEntry desc',
diff --git a/apps/web/src/app/features/banking/payment-types.ts b/apps/web/src/app/features/banking/payment-types.ts
index 1cf9fe6..1bb43bf 100644
--- a/apps/web/src/app/features/banking/payment-types.ts
+++ b/apps/web/src/app/features/banking/payment-types.ts
@@ -28,6 +28,8 @@ export interface PaymentScreen {
localCurrency: string;
/** País de los bancos de los cheques. */
countryCode: string;
+ /** Admite tarjetas de crédito como medio de pago (los pagos efectuados no). */
+ cards: boolean;
columns: readonly ListColumn[];
filters: readonly ListFilter[];
orderby: string;