Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/eclipxe13/CfdiUtils/llms.txt

Use this file to discover all available pages before exploring further.

The CFDI Complement for Payment Reception version 2.0 (Complemento para recepción de Pagos 2.0) requires several computed totals that aggregate taxes across all Pago and DocumentoRelacionado nodes. These calculations involve currency conversion factors (TipoCambioP, EquivalenciaDR), multiple tax types, and decimal truncation rules that are difficult to implement correctly using ordinary floating-point arithmetic. CfdiUtils\SumasPagos20\Calculator performs all these calculations precisely, and CfdiUtils\SumasPagos20\PagosWriter writes the results back into the corresponding XML nodes. The static convenience method PagosWriter::calculateAndPut() combines both steps in a single call. The three groups of data that get calculated and written are:
  • Totales — the aggregate tax totals written as attributes of the pago20:Totales node.
  • Pago\ImpuestosP — per-payment tax nodes derived from the related document taxes.
  • Pago@Monto — the minimum payment amount if the attribute was not already set.
The ext-bcmath PHP extension is required for this utility. All decimal arithmetic in the Pagos 2.0 calculator is delegated to BCMath (Arbitrary Precision Mathematics) to avoid the rounding and truncation errors inherent in IEEE 754 floating-point. Ensure extension=bcmath is enabled in your php.ini before using this utility.

Quick Start

<?php

use CfdiUtils\SumasPagos20\PagosWriter;

/**
 * $pagos is the pago20:Pagos element already populated with
 * Pago nodes, DoctoRelacionado nodes, and ImpuestosDR data.
 *
 * @var \CfdiUtils\Elements\Pagos20\Pagos $pagos
 */

// One-liner: calculate everything and write it into $pagos
PagosWriter::calculateAndPut($pagos);

Calculator Class

CfdiUtils\SumasPagos20\Calculator is the calculation engine. Construct it when you need control over precision or supported currencies.
<?php

use CfdiUtils\SumasPagos20\Calculator;
use CfdiUtils\SumasPagos20\Currencies;
use CfdiUtils\SumasPagos20\PagosWriter;

$calculator = new Calculator(
    2,                                          // $paymentTaxesPrecision — decimal places for payment taxes
    new Currencies(['MXN' => 2, 'USD' => 2, 'EUR' => 2]) // currencies with their decimal precision
);

// Calculate — returns a Pagos result object (immutable)
$result = $calculator->calculate($pagos);

// Write the results into the XML nodes
$writer = new PagosWriter($pagos);
$writer->put($result);

Constructor parameters

paymentTaxesPrecision
int
default:"6"
Number of decimal places used when rounding per-payment tax amounts (applied to each Pago\ImpuestosP value). Must be between 0 and 6 — values outside this range are clamped automatically.
currencies
Currencies
default:"new Currencies([\"MXN\" => 2, \"USD\" => 2])"
A Currencies instance mapping currency codes to their decimal precision. Used when computing the minimum Pago@Monto via truncation. Add any foreign currencies used in your payments.

calculate(NodeInterface $nodePagos): Pagos

Reads all pago20:Pago children of $nodePagos, processes each pago20:DoctoRelacionado and its ImpuestosDR descendants, and returns a Pagos result object containing the computed Totales and per-Pago data. The returned Pagos object is immutable and JSON-serializable — useful for logging or debugging.

Available Totals

The Totales object returned by $result->getTotales() exposes the following getters. Any that return null indicate no data exists for that field (i.e. the corresponding XML attribute should be omitted).

Transfer taxes (IVA)

getTrasladoIva16Base(): ?Decimal
?Decimal
Corresponds to XML attribute TotalTrasladosBaseIVA16. Aggregate base amount for IVA at 16 %.
getTrasladoIva16Importe(): ?Decimal
?Decimal
Corresponds to XML attribute TotalTrasladosImpuestoIVA16. Aggregate tax amount for IVA at 16 %.
getTrasladoIva08Base(): ?Decimal
?Decimal
Corresponds to XML attribute TotalTrasladosBaseIVA8. Aggregate base amount for IVA at 8 %.
getTrasladoIva08Importe(): ?Decimal
?Decimal
Corresponds to XML attribute TotalTrasladosImpuestoIVA8. Aggregate tax amount for IVA at 8 %.
getTrasladoIva00Base(): ?Decimal
?Decimal
Corresponds to XML attribute TotalTrasladosBaseIVA0. Aggregate base amount for IVA at 0 %.
getTrasladoIva00Importe(): ?Decimal
?Decimal
Corresponds to XML attribute TotalTrasladosImpuestoIVA0. Aggregate tax amount for IVA at 0 %.
getTrasladoIvaExento(): ?Decimal
?Decimal
Corresponds to XML attribute TotalTrasladosBaseIVAExento. Aggregate base amount for exempt IVA entries.

Withholding taxes (retenciones)

getRetencionIva(): ?Decimal
?Decimal
Corresponds to XML attribute TotalRetencionesIVA. Total IVA withheld across all payments (impuesto 002).
getRetencionIsr(): ?Decimal
?Decimal
Corresponds to XML attribute TotalRetencionesISR. Total ISR withheld across all payments (impuesto 001).
getRetencionIeps(): ?Decimal
?Decimal
Corresponds to XML attribute TotalRetencionesIEPS. Total IEPS withheld across all payments (impuesto 003).

Grand total

getTotal(): Decimal
Decimal
Corresponds to XML attribute MontoTotalPagos. Sum of all Pago@Monto values converted to MXN and rounded to 2 decimal places.

The Decimal Type

All monetary values inside the result objects are instances of CfdiUtils\SumasPagos20\Decimal — a BCMath-backed arbitrary-precision decimal type.
<?php

$totales = $result->getTotales();

// Decimal implements Stringable
$base16 = $totales->getTrasladoIva16Base();
if ($base16 !== null) {
    echo (string) $base16;              // e.g. "10000.00"

    // Round to a specific number of decimal places
    $rounded = $base16->round(2);
    echo (string) $rounded;             // e.g. "10000.00"
}
Decimal is designed for internal use — you will typically only read its string representation when inspecting computed values.

Accessing Per-Payment Results

The Pagos result object also gives you access to the computed data for each individual payment. Use Impuestos::getTraslados() and Impuestos::getRetenciones() to iterate safely over all taxes, or use find() to look up a specific tax without risking an exception.
<?php

$result = $calculator->calculate($pagos);

// Access all computed payments
foreach ($result->getPagos() as $index => $pago) {
    echo 'Pago #', $index, PHP_EOL;
    echo '  Monto mínimo : ', $pago->getMontoMinimo(), PHP_EOL;
    echo '  Monto usado  : ', $pago->getMonto(), PHP_EOL;
    echo '  Tipo cambio  : ', $pago->getTipoCambioP(), PHP_EOL;

    $impuestos = $pago->getImpuestos();

    // Iterate all transfer taxes for this payment
    foreach ($impuestos->getTraslados() as $traslado) {
        echo '  Traslado Impuesto : ', $traslado->getImpuesto(), PHP_EOL;
        echo '  Traslado Base     : ', $traslado->getBase(), PHP_EOL;
        echo '  Traslado Importe  : ', $traslado->getImporte(), PHP_EOL;
    }

    // Iterate all withholding taxes for this payment
    foreach ($impuestos->getRetenciones() as $retencion) {
        echo '  Retencion Impuesto: ', $retencion->getImpuesto(), PHP_EOL;
        echo '  Retencion Importe : ', $retencion->getImporte(), PHP_EOL;
    }
}

// Or access a specific payment by index
$firstPago = $result->getPago(0);
Impuestos::getTraslado(string $impuesto, string $tipoFactor, string $tasaCuota): Impuesto and Impuestos::getRetencion(string $impuesto): Impuesto throw a LogicException if the requested tax does not exist. Use getTraslados() / getRetenciones() to iterate all entries without risk, or wrap the call in a try/catch if you need to look up a specific tax that may be absent.

Required Input Data

Before calling calculate(), the Pagos element must already contain the following data:
pago20:Pagos
  pago20:Pago
    @MonedaP          — Currency code of the payment (e.g. 'MXN', 'USD')
    @TipoCambioP      — Exchange rate to MXN
    @Monto            — Payment amount (optional; minimum is computed if absent)
    pago20:DoctoRelacionado
      @ImpPagado      — Amount paid on this related document
      @EquivalenciaDR — Exchange factor from document currency to payment currency
      pago20:ImpuestosDR
        pago20:TrasladosDR
          pago20:TrasladoDR
            @ImpuestoDR, @TipoFactorDR, @TasaOCuotaDR, @BaseDR, @ImporteDR
        pago20:RetencionesDR
          pago20:RetencionDR
            @ImpuestoDR, @ImporteDR

Using PagosWriter Directly

If you already have a Pagos result object, PagosWriter lets you write it independently:
<?php

use CfdiUtils\SumasPagos20\Calculator;
use CfdiUtils\SumasPagos20\PagosWriter;

$calculator = new Calculator();
$result     = $calculator->calculate($complementoPagos);

// Inspect result before writing
echo json_encode($result, JSON_PRETTY_PRINT);

// Write totals and per-pago taxes
$writer = new PagosWriter($complementoPagos);
$writer->put($result);
PagosWriter::put(Pagos $result): void performs two operations:
  1. Writes pago20:Totales — removes any existing Totales node and adds a new one with the calculated attributes (MontoTotalPagos, TotalRetencionesIVA, TotalTrasladosBaseIVA16, etc.).
  2. Writes each pago20:Pago\ImpuestosP — removes any existing ImpuestosP node and populates RetencionesP and TrasladosP children from the calculated per-payment taxes.
PagosWriter only writes the Pago@Monto attribute when the attribute is not already set on the element. If you have manually set a Monto that is higher than the computed minimum, it will be preserved.

Debugging with JSON

All result objects implement JsonSerializable, making them easy to inspect:
<?php

$result = $calculator->calculate($pagos);

// Pretty-print the full calculation result
echo json_encode($result, JSON_PRETTY_PRINT);

// Pretty-print just the totals
echo json_encode($result->getTotales(), JSON_PRETTY_PRINT);
Example output:
{
  "totales": {
    "trasladoIva16Base": "10000.00",
    "trasladoIva16Importe": "1600.00",
    "total": "11600.00"
  },
  "pagos": [
    {
      "monto": "11600.00",
      "montoMinimo": "11600.00",
      "tipoCambioP": "1"
    }
  ]
}

BCMath and Decimal Precision

Working with payment totals requires decimal arithmetic that ordinary PHP floats cannot handle reliably. For example, truncating 12345.6789 to 2 decimal places with floats can yield 12345.67 or 12345.68 depending on the internal representation. Calculator uses BCMath for all arithmetic — addition, multiplication, division, truncation, and rounding — treating every number as a string of digits throughout the computation. Final totals are rounded to 2 decimal places (MXN) and per-payment taxes to $paymentTaxesPrecision decimal places.
For most production invoices, the default paymentTaxesPrecision = 6 is the right choice. Lower values reduce the precision of intermediate calculations and may cause the final totals to diverge from those produced by the SAT’s validator.
For the full workflow of creating a CFDI with a Pagos 2.0 complement — including building the Pagos element, adding Pago and DoctoRelacionado nodes, and sealing the document — see the Creating CFDI with Complements guide.

Build docs developers (and LLMs) love