Skip to main content

Overview

The payment system (abonos) allows customers to make payments against credit sales. Each payment reduces the outstanding balance and can be applied to specific installments in the payment plan. The system automatically updates balances and marks installments as paid through database triggers.

Payment Entity

The AbonosVenta entity represents a single payment transaction.

Entity Fields

Id
int
required
Auto-generated payment identifier (primary key)
Norden
string
required
Sale order reference (foreign key to ventas)
Fecha
DateTime
Payment date and time (defaults to current datetime)
Monto
decimal
required
Payment amount received
NumeroCuota
int
required
Installment number this payment applies to

Relationships

  • NordenNavigation: Parent sale record (Venta)

Recording a Payment

1

Verify Sale Information

Before recording a payment, verify:
  • The sale exists and is a credit sale (TipoPago = 'CREDITO')
  • The sale is not voided (Nula = 0)
  • The sale has an outstanding balance (Saldo > 0)
var sale = await context.Ventas
    .FirstOrDefaultAsync(v => v.Norden == orderNumber);

if (sale == null || sale.TipoPago != "CREDITO") {
    throw new InvalidOperationException("Invalid credit sale");
}

if (sale.Nula == true) {
    throw new InvalidOperationException("Cannot record payment for voided sale");
}

if (sale.Saldo <= 0) {
    throw new InvalidOperationException("Sale is already paid in full");
}
2

Identify Target Installment

Determine which installment the payment is for. Typically, this would be the next unpaid installment:
var nextInstallment = await context.PlanPagos
    .Where(p => p.Norden == orderNumber && p.Pagada == false)
    .OrderBy(p => p.NumeroCuota)
    .FirstOrDefaultAsync();

if (nextInstallment == null) {
    throw new InvalidOperationException("No pending installments found");
}
3

Validate Payment Amount

Ensure the payment amount is valid:
if (amount <= 0) {
    throw new ArgumentException("Payment amount must be greater than zero");
}

if (amount > sale.Saldo) {
    throw new ArgumentException(
        $"Payment amount ({amount}) exceeds outstanding balance ({sale.Saldo})"
    );
}
4

Create Payment Record

Insert the payment record into the AbonosVenta table:
var payment = new AbonosVenta {
    Norden = orderNumber,
    Fecha = DateTime.Now,
    Monto = amount,
    NumeroCuota = nextInstallment.NumeroCuota
};

context.AbonosVentas.Add(payment);
await context.SaveChangesAsync();
5

Automatic Updates via Trigger

When the payment is inserted, the TR_ActualizarSaldo trigger automatically:
  1. Reduces the sale balance:
    UPDATE ventas
    SET saldo = saldo - payment.monto
    WHERE norden = payment.norden
    
  2. Marks the installment as paid:
    UPDATE planPagos
    SET pagada = 1
    WHERE norden = payment.norden 
      AND numeroCuota = payment.numeroCuota
    
No additional code is required - the database handles these updates automatically.

Payment Workflow Example

Consider a credit sale with the following details: Sale Information:
  • Order Number: ORD-2026-001
  • Total: $1,236.00 (including interest)
  • Months: 4
  • Monthly Payment: $309.00
  • Current Balance: $1,236.00
Recording First Payment:
var payment1 = new AbonosVenta {
    Norden = "ORD-2026-001",
    Fecha = DateTime.Now,
    Monto = 309.00m,
    NumeroCuota = 1
};

context.AbonosVentas.Add(payment1);
await context.SaveChangesAsync();
After Trigger Execution:
  • Sale balance updated: 1,236.001,236.00 - 309.00 = $927.00
  • Installment #1 marked as paid (Pagada = 1)
Recording Second Payment:
var payment2 = new AbonosVenta {
    Norden = "ORD-2026-001",
    Fecha = DateTime.Now,
    Monto = 309.00m,
    NumeroCuota = 2
};

context.AbonosVentas.Add(payment2);
await context.SaveChangesAsync();
After Trigger Execution:
  • Sale balance updated: 927.00927.00 - 309.00 = $618.00
  • Installment #2 marked as paid (Pagada = 1)

Partial Payments

The system allows partial payments that don’t cover the full installment amount:
var partialPayment = new AbonosVenta {
    Norden = "ORD-2026-001",
    Fecha = DateTime.Now,
    Monto = 150.00m,  // Less than the $309.00 installment
    NumeroCuota = 3
};
Important Notes:
  • The trigger will still mark the installment as paid (Pagada = 1)
  • The balance will be reduced by the actual amount paid ($150.00)
  • You may need business logic to handle partially paid installments
  • Consider tracking partial payments separately if needed

Overpayments

If a customer pays more than the installment amount:
var overpayment = new AbonosVenta {
    Norden = "ORD-2026-001",
    Fecha = DateTime.Now,
    Monto = 450.00m,  // More than the $309.00 installment
    NumeroCuota = 3
};
Result:
  • Installment #3 is marked as paid
  • Balance is reduced by the full $450.00
  • Excess amount reduces the overall balance
  • Customer still owes remaining installments, but total debt is lower

Payment History

View all payments for a sale:
var paymentHistory = await context.AbonosVentas
    .Where(a => a.Norden == orderNumber)
    .OrderBy(a => a.Fecha)
    .Select(a => new {
        a.Id,
        a.Fecha,
        a.Monto,
        a.NumeroCuota
    })
    .ToListAsync();
Example Output:
Payment IDDateAmountInstallment
12026-02-15$309.001
22026-03-15$309.002
32026-04-10$150.003
42026-04-20$159.003

Account Statement

Generate a complete account statement showing sales and payments:
var statement = await context.Ventas
    .Where(v => v.Codclien == customerCode && v.TipoPago == "CREDITO")
    .Select(v => new {
        v.Norden,
        v.Fecha,
        v.Total,
        v.Saldo,
        Payments = v.AbonosVenta.Select(a => new {
            a.Fecha,
            a.Monto,
            a.NumeroCuota
        }).ToList(),
        TotalPaid = v.AbonosVenta.Sum(a => a.Monto)
    })
    .ToListAsync();

Database Trigger Details

The TR_ActualizarSaldo trigger handles automatic updates:
CREATE TRIGGER TR_ActualizarSaldo
ON abonosVentas
AFTER INSERT
AS
BEGIN
    -- Update sale balance
    UPDATE v
    SET v.saldo = v.saldo - i.monto
    FROM ventas v
    INNER JOIN inserted i ON v.norden = i.norden

    -- Mark installment as paid
    UPDATE pp
    SET pagada = 1
    FROM planPagos pp
    INNER JOIN inserted i 
        ON pp.norden = i.norden 
        AND pp.numeroCuota = i.numeroCuota
END
This ensures data consistency without requiring additional application code.

Cascade Deletion

Payments are configured with ON DELETE CASCADE:
FOREIGN KEY (norden) REFERENCES ventas(norden) ON DELETE CASCADE
This means:
  • Deleting a sale automatically deletes all associated payment records
  • Ensures referential integrity
  • Simplifies data cleanup for voided sales

Database Schema

CREATE TABLE abonosVentas(
    id INT IDENTITY(1,1) PRIMARY KEY,
    norden VARCHAR(50) NOT NULL,
    fecha DATETIME DEFAULT GETDATE(),
    monto DECIMAL(18,2) NOT NULL,
    numeroCuota INT NOT NULL,
    FOREIGN KEY (norden) REFERENCES ventas(norden) ON DELETE CASCADE
)

Performance Optimization

The database includes optimized indexes:
-- Find payments by sale order
CREATE INDEX IDX_abonosVentas_norden ON abonosVentas(norden)

-- Query payments by date for reports
CREATE INDEX IDX_abonosVentas_fecha ON abonosVentas(fecha)

Best Practices

Always validate payment data before inserting:
// Check amount is positive
if (amount <= 0) throw new ArgumentException("Invalid amount");

// Verify installment exists
var installment = await context.PlanPagos
    .FirstOrDefaultAsync(p => 
        p.Norden == orderNumber && 
        p.NumeroCuota == installmentNumber
    );

if (installment == null) {
    throw new InvalidOperationException("Installment not found");
}
Consider adding a payment method field to track how customers pay:
public enum PaymentMethod {
    Cash,
    Check,
    CreditCard,
    BankTransfer,
    MobilePay
}

// Add to AbonosVenta model
public PaymentMethod? MetodoPago { get; set; }
Create payment receipts for customer records:
var receipt = new PaymentReceipt {
    ReceiptNumber = GenerateReceiptNumber(),
    PaymentId = payment.Id,
    CustomerName = sale.CodclienNavigation.Nombre,
    Amount = payment.Monto,
    Date = payment.Fecha,
    OrderNumber = payment.Norden,
    InstallmentNumber = payment.NumeroCuota
};
Use database transactions to ensure consistency:
using var transaction = await context.Database.BeginTransactionAsync();
try {
    context.AbonosVentas.Add(payment);
    await context.SaveChangesAsync();
    
    // Additional operations if needed
    
    await transaction.CommitAsync();
} catch {
    await transaction.RollbackAsync();
    throw;
}

Build docs developers (and LLMs) love