Ir al contenido

SDK y modelosSDK y modelos: PHP

SDK y modelos: PHP

Ver como Markdown

Clases final con propiedades readonly, jsonSerialize() para armar el JSON del request y fromArray() para leer la respuesta. Requiere PHP 8.1 o superior; el espacio de nombres es NovaFE\Client\Models.

emitir.php
<?php
declare(strict_types=1);
use NovaFE\Client\Models\EcfBuyerPayload;
use NovaFE\Client\Models\EcfDto;
use NovaFE\Client\Models\EcfLinePayload;
use NovaFE\Client\Models\EcfPaymentMethodPayload;
use NovaFE\Client\Models\EcfPaymentPayload;
use NovaFE\Client\Models\IssueEcfCommand;
$payload = new IssueEcfCommand(
type: 31,
lines: [
new EcfLinePayload(
name: "Servicio de consultoría",
kind: "service",
quantity: 1.0,
unitOfMeasure: "43",
unitPrice: 2000.0,
itbisRate: 1,
),
],
incomeType: "01",
internalNumber: "FAC-2026-00042",
buyer: new EcfBuyerPayload(rnc: "131880681", name: "Mi Cliente SRL"),
payment: new EcfPaymentPayload(
condition: "credit",
dueDate: "15-03-2026",
methods: [new EcfPaymentMethodPayload(type: "check_transfer", amount: 2360.0)],
),
);
$curl = curl_init("https://api.novafe.example/api/v1/ecf");
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: " . getenv("NOVAFE_API_KEY"),
"Idempotency-Key: " . bin2hex(random_bytes(16)),
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$ecf = EcfDto::fromArray(json_decode(curl_exec($curl), true));
echo $ecf->encf . " " . $ecf->status . PHP_EOL;

Todo el contenido de Ecf.php, listo para copiar. Incluye IssueEcfCommand (el cuerpo de POST /ecf), EcfDto (la respuesta) y sus tipos anidados.

Ecf.php
<?php
// Generado desde el documento OpenAPI publico de NovaFE. No lo edites a mano: regeneralo con `bun run models`.
declare(strict_types=1);
namespace NovaFE\Client\Models;
use JsonSerializable;
final class DgiiMessage implements JsonSerializable
{
public function __construct(
public readonly int $code,
public readonly string $value,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'code' => $this->code,
'value' => $this->value,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
code: $data['code'],
value: $data['value'],
);
}
}
/** Una entrada de `/{amb}/consultatrackids/api/trackids/consulta`. Puede haber más de una para el mismo e-NCF si se remitió varias veces. */
final class DgiiTrackIdEntry implements JsonSerializable
{
public function __construct(
public readonly string $trackId,
public readonly string $estado,
public readonly ?string $fechaRecepcion = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'trackId' => $this->trackId,
'estado' => $this->estado,
'fechaRecepcion' => $this->fechaRecepcion,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
trackId: $data['trackId'],
estado: $data['estado'],
fechaRecepcion: isset($data['fechaRecepcion']) ? $data['fechaRecepcion'] : null,
);
}
}
/** Texto libre para la Representación Impresa. */
final class EcfAdditionalInfoPayload implements JsonSerializable
{
public function __construct(
public readonly ?string $issuer = null,
public readonly ?string $buyer = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'issuer' => $this->issuer,
'buyer' => $this->buyer,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
issuer: isset($data['issuer']) ? $data['issuer'] : null,
buyer: isset($data['buyer']) ? $data['buyer'] : null,
);
}
}
/** Desglose de impuestos adicionales por código de la Tabla I (001–039). Todos los montos los trae el cliente ya calculados. `iscEspecifico` (monto fijo por volumen: alcoholes 006-018, cigarrillos 019-022) integra la base imponible del ITBIS de esa línea; `iscAdvalorem` y `otros` (Propina, CDT, ISC de servicios, Primera Placa…) van "por encima", sin tocar la base. */
final class EcfAdditionalTaxPayload implements JsonSerializable
{
public function __construct(
public readonly string $code,
public readonly ?float $rate = null,
public readonly ?float $iscEspecifico = null,
public readonly ?float $iscAdvalorem = null,
public readonly ?float $otros = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'code' => $this->code,
'rate' => $this->rate,
'iscEspecifico' => $this->iscEspecifico,
'iscAdvalorem' => $this->iscAdvalorem,
'otros' => $this->otros,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
code: $data['code'],
rate: isset($data['rate']) ? (float) $data['rate'] : null,
iscEspecifico: isset($data['iscEspecifico']) ? (float) $data['iscEspecifico'] : null,
iscAdvalorem: isset($data['iscAdvalorem']) ? (float) $data['iscAdvalorem'] : null,
otros: isset($data['otros']) ? (float) $data['otros'] : null,
);
}
}
/** Un renglón del historial de envíos de un comprobante: cómo terminó un ciclo contra la DGII. Lo que emite `GET /api/v1/ecf/{id}/attempts`. */
final class EcfAttemptDto implements JsonSerializable
{
public function __construct(
/** 1, 2, 3… dentro del comprobante. */
public readonly int $attempt,
/** En qué estado dejó el ciclo al comprobante: `accepted`, `accepted_conditional`, `rejected`, `review` o `failed`. */
public readonly string $outcome,
/** @var DgiiMessage[] Observaciones o motivo de rechazo de la DGII; o el motivo del fallo si no hubo veredicto. */
public readonly array $messages,
/** SHA-256 (hex) del XML enviado en este ciclo. */
public readonly string $xmlSha256,
/** Cuándo se registró el resultado. */
public readonly string $recordedAt,
/** `TrackId` de la DGII de este ciclo; ausente si no llegó a haber uno. */
public readonly ?string $trackId = null,
/** Código de estado de la DGII (1 aceptado, 2 rechazado, 4 aceptado condicional); ausente si no hubo veredicto. */
public readonly ?int $statusCode = null,
/** El `estado` textual de la DGII. */
public readonly ?string $statusText = null,
/** `secuenciaUtilizada` de la DGII: `false` = el e-NCF no se consumió; `true` o ausente = consumido. */
public readonly ?bool $sequenceUsed = null,
/** Qué se decidió con un rechazo: `auto:<código>` (la DGII falló de forma transitoria y NovaFE reenvía el mismo XML solo, p. ej. `auto:4`) o `exhausted` (era reintentable pero se agotó el ladder y el rechazo quedó firme). Ausente en el resto de resultados. */
public readonly ?string $retryDecision = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'attempt' => $this->attempt,
'outcome' => $this->outcome,
'trackId' => $this->trackId,
'statusCode' => $this->statusCode,
'statusText' => $this->statusText,
'sequenceUsed' => $this->sequenceUsed,
'messages' => $this->messages,
'retryDecision' => $this->retryDecision,
'xmlSha256' => $this->xmlSha256,
'recordedAt' => $this->recordedAt,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
attempt: $data['attempt'],
outcome: $data['outcome'],
messages: array_map(fn ($i) => DgiiMessage::fromArray($i), $data['messages']),
xmlSha256: $data['xmlSha256'],
recordedAt: $data['recordedAt'],
trackId: isset($data['trackId']) ? $data['trackId'] : null,
statusCode: isset($data['statusCode']) ? $data['statusCode'] : null,
statusText: isset($data['statusText']) ? $data['statusText'] : null,
sequenceUsed: isset($data['sequenceUsed']) ? $data['sequenceUsed'] : null,
retryDecision: isset($data['retryDecision']) ? $data['retryDecision'] : null,
);
}
}
/** Bloque `<Comprador>`. `Rnc` y `ForeignId` son excluyentes. */
final class EcfBuyerPayload implements JsonSerializable
{
public function __construct(
public readonly ?string $name = null,
public readonly ?string $rnc = null,
public readonly ?string $foreignId = null,
public readonly ?string $email = null,
public readonly ?string $contact = null,
public readonly ?string $address = null,
public readonly ?string $municipality = null,
public readonly ?string $province = null,
public readonly ?string $additionalInfo = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'name' => $this->name,
'rnc' => $this->rnc,
'foreignId' => $this->foreignId,
'email' => $this->email,
'contact' => $this->contact,
'address' => $this->address,
'municipality' => $this->municipality,
'province' => $this->province,
'additionalInfo' => $this->additionalInfo,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
name: isset($data['name']) ? $data['name'] : null,
rnc: isset($data['rnc']) ? $data['rnc'] : null,
foreignId: isset($data['foreignId']) ? $data['foreignId'] : null,
email: isset($data['email']) ? $data['email'] : null,
contact: isset($data['contact']) ? $data['contact'] : null,
address: isset($data['address']) ? $data['address'] : null,
municipality: isset($data['municipality']) ? $data['municipality'] : null,
province: isset($data['province']) ? $data['province'] : null,
additionalInfo: isset($data['additionalInfo']) ? $data['additionalInfo'] : null,
);
}
}
/** Totales que calculó el cliente (en DOP). Se comparan con el cálculo de NovaFE (tolerancia RF-06.6); el valor de NovaFE es el que va al XML. Todo opcional. */
final class EcfDeclaredTotalsPayload implements JsonSerializable
{
public function __construct(
public readonly ?float $montoGravadoTotal = null,
public readonly ?float $montoExento = null,
public readonly ?float $totalItbis = null,
public readonly ?float $montoImpuestoAdicional = null,
public readonly ?float $montoTotal = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'montoGravadoTotal' => $this->montoGravadoTotal,
'montoExento' => $this->montoExento,
'totalItbis' => $this->totalItbis,
'montoImpuestoAdicional' => $this->montoImpuestoAdicional,
'montoTotal' => $this->montoTotal,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
montoGravadoTotal: isset($data['montoGravadoTotal']) ? (float) $data['montoGravadoTotal'] : null,
montoExento: isset($data['montoExento']) ? (float) $data['montoExento'] : null,
totalItbis: isset($data['totalItbis']) ? (float) $data['totalItbis'] : null,
montoImpuestoAdicional: isset($data['montoImpuestoAdicional']) ? (float) $data['montoImpuestoAdicional'] : null,
montoTotal: isset($data['montoTotal']) ? (float) $data['montoTotal'] : null,
);
}
}
/** El intercambio con la DGII de un comprobante: lo que ella respondió más los instantes en que NovaFE registró cada paso. */
final class EcfDgiiExchange implements JsonSerializable
{
public function __construct(
/** @var DgiiMessage[] Observaciones o motivo de rechazo que devolvió la DGII. */
public readonly array $messages,
/** Identificador que la DGII asigna al recibir el comprobante. */
public readonly ?string $trackId = null,
/** El `estado` textual de la DGII ("Aceptado", "Rechazado", "En Proceso"…); en el idioma en que lo manda ella. */
public readonly ?string $status = null,
/** Código de estado de la DGII: 1 aceptado · 2 rechazado · 3 en proceso · 4 aceptado condicional. */
public readonly ?int $statusCode = null,
/** `secuenciaUtilizada` de la DGII: `false` = el e-NCF no se consumió (firma/XML inválidos); `true` o ausente = consumido. */
public readonly ?bool $sequenceUsed = null,
/** Instante en que la DGII confirmó la recepción (hay string? EcfDgiiExchange.TrackId). */
public readonly ?string $submittedAt = null,
/** La `fechaRecepcion` que informó la DGII; ausente si no la dio (p. ej. RFCE). */
public readonly ?string $receivedAt = null,
/** Instante en que la DGII dio un resultado definitivo. */
public readonly ?string $processedAt = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'trackId' => $this->trackId,
'status' => $this->status,
'statusCode' => $this->statusCode,
'sequenceUsed' => $this->sequenceUsed,
'messages' => $this->messages,
'submittedAt' => $this->submittedAt,
'receivedAt' => $this->receivedAt,
'processedAt' => $this->processedAt,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
messages: array_map(fn ($i) => DgiiMessage::fromArray($i), $data['messages']),
trackId: isset($data['trackId']) ? $data['trackId'] : null,
status: isset($data['status']) ? $data['status'] : null,
statusCode: isset($data['statusCode']) ? $data['statusCode'] : null,
sequenceUsed: isset($data['sequenceUsed']) ? $data['sequenceUsed'] : null,
submittedAt: isset($data['submittedAt']) ? $data['submittedAt'] : null,
receivedAt: isset($data['receivedAt']) ? $data['receivedAt'] : null,
processedAt: isset($data['processedAt']) ? $data['processedAt'] : null,
);
}
}
/** Vista de un comprobante emitido (respuesta de `POST /ecf` y `GET /ecf/{id}`). Trae la identidad fiscal, el resumen comercial (comprador, monto total — igual que EcfSummaryDto) y el resultado del intercambio con la DGII (EcfDgiiExchange? EcfDto.Dgii). El desglose por línea y el resto del detalle comercial siguen viviendo solo en el XML firmado (`GET /ecf/{id}/xml`). */
final class EcfDto implements JsonSerializable
{
public function __construct(
public readonly string $id,
public readonly string $status,
public readonly string $encf,
public readonly int $type,
public readonly string $environment,
public readonly string $issueDate,
public readonly string $issuedAt,
public readonly string $signedAt,
public readonly string $securityCode,
public readonly string $qrUrl,
public readonly bool $submitsRfce,
public readonly bool $signedDuringContingency,
public readonly float $montoTotal,
public readonly string $documentHash,
public readonly ?string $sequenceExpiresOn = null,
public readonly ?string $internalNumber = null,
public readonly ?string $toleranceWarning = null,
public readonly ?string $buyerRnc = null,
public readonly ?string $buyerName = null,
/** Lo que pasó con la DGII: `trackId`, estado (texto y código), mensajes, `secuenciaUtilizada` y los instantes de envío, recepción y resolución. `null` mientras el comprobante no se haya enviado. */
public readonly ?EcfDgiiExchange $dgii = null,
/** El reintento automático en curso o ya hecho: cuántas veces NovaFE reenvió el mismo comprobante tras un rechazo transitorio de la DGII, el tope, y cuándo es el próximo intento. `null` mientras no haya habido ningún reintento. */
public readonly ?EcfRetry $retry = null,
/** Enlaces a los recursos relacionados del comprobante. */
public readonly ?EcfLinks $links = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'id' => $this->id,
'status' => $this->status,
'encf' => $this->encf,
'type' => $this->type,
'environment' => $this->environment,
'sequenceExpiresOn' => $this->sequenceExpiresOn,
'issueDate' => $this->issueDate,
'issuedAt' => $this->issuedAt,
'signedAt' => $this->signedAt,
'securityCode' => $this->securityCode,
'qrUrl' => $this->qrUrl,
'submitsRfce' => $this->submitsRfce,
'internalNumber' => $this->internalNumber,
'toleranceWarning' => $this->toleranceWarning,
'signedDuringContingency' => $this->signedDuringContingency,
'montoTotal' => $this->montoTotal,
'buyerRnc' => $this->buyerRnc,
'buyerName' => $this->buyerName,
'documentHash' => $this->documentHash,
'dgii' => $this->dgii,
'retry' => $this->retry,
'links' => $this->links,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
id: $data['id'],
status: $data['status'],
encf: $data['encf'],
type: $data['type'],
environment: $data['environment'],
issueDate: $data['issueDate'],
issuedAt: $data['issuedAt'],
signedAt: $data['signedAt'],
securityCode: $data['securityCode'],
qrUrl: $data['qrUrl'],
submitsRfce: $data['submitsRfce'],
signedDuringContingency: $data['signedDuringContingency'],
montoTotal: (float) $data['montoTotal'],
documentHash: $data['documentHash'],
sequenceExpiresOn: isset($data['sequenceExpiresOn']) ? $data['sequenceExpiresOn'] : null,
internalNumber: isset($data['internalNumber']) ? $data['internalNumber'] : null,
toleranceWarning: isset($data['toleranceWarning']) ? $data['toleranceWarning'] : null,
buyerRnc: isset($data['buyerRnc']) ? $data['buyerRnc'] : null,
buyerName: isset($data['buyerName']) ? $data['buyerName'] : null,
dgii: isset($data['dgii']) ? EcfDgiiExchange::fromArray($data['dgii']) : null,
retry: isset($data['retry']) ? EcfRetry::fromArray($data['retry']) : null,
links: isset($data['links']) ? EcfLinks::fromArray($data['links']) : null,
);
}
}
/** Datos de exportación (solo tipo 46). */
final class EcfExportPayload implements JsonSerializable
{
public function __construct(
public readonly ?string $loadingPortName = null,
public readonly ?string $deliveryTerms = null,
public readonly ?float $totalFob = null,
public readonly ?float $insurance = null,
public readonly ?float $freight = null,
public readonly ?float $otherCharges = null,
public readonly ?float $totalCif = null,
public readonly ?string $customsRegime = null,
public readonly ?string $departurePortName = null,
public readonly ?string $unloadingPortName = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'loadingPortName' => $this->loadingPortName,
'deliveryTerms' => $this->deliveryTerms,
'totalFob' => $this->totalFob,
'insurance' => $this->insurance,
'freight' => $this->freight,
'otherCharges' => $this->otherCharges,
'totalCif' => $this->totalCif,
'customsRegime' => $this->customsRegime,
'departurePortName' => $this->departurePortName,
'unloadingPortName' => $this->unloadingPortName,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
loadingPortName: isset($data['loadingPortName']) ? $data['loadingPortName'] : null,
deliveryTerms: isset($data['deliveryTerms']) ? $data['deliveryTerms'] : null,
totalFob: isset($data['totalFob']) ? (float) $data['totalFob'] : null,
insurance: isset($data['insurance']) ? (float) $data['insurance'] : null,
freight: isset($data['freight']) ? (float) $data['freight'] : null,
otherCharges: isset($data['otherCharges']) ? (float) $data['otherCharges'] : null,
totalCif: isset($data['totalCif']) ? (float) $data['totalCif'] : null,
customsRegime: isset($data['customsRegime']) ? $data['customsRegime'] : null,
departurePortName: isset($data['departurePortName']) ? $data['departurePortName'] : null,
unloadingPortName: isset($data['unloadingPortName']) ? $data['unloadingPortName'] : null,
);
}
}
/** Facturación en divisa (`<OtraMoneda>`). El cliente trae los montos ya convertidos. */
final class EcfForeignCurrencyPayload implements JsonSerializable
{
public function __construct(
public readonly string $currency,
public readonly float $exchangeRate,
public readonly EcfForeignCurrencyTotalsPayload $totals,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'currency' => $this->currency,
'exchangeRate' => $this->exchangeRate,
'totals' => $this->totals,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
currency: $data['currency'],
exchangeRate: (float) $data['exchangeRate'],
totals: EcfForeignCurrencyTotalsPayload::fromArray($data['totals']),
);
}
}
final class EcfForeignCurrencyTotalsPayload implements JsonSerializable
{
public function __construct(
public readonly ?float $montoGravadoTotal = null,
public readonly ?float $montoGravadoI1 = null,
public readonly ?float $montoGravadoI2 = null,
public readonly ?float $montoGravadoI3 = null,
public readonly ?float $montoExento = null,
public readonly ?float $totalItbis = null,
public readonly ?float $totalItbis1 = null,
public readonly ?float $totalItbis2 = null,
public readonly ?float $totalItbis3 = null,
public readonly ?float $montoTotal = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'montoGravadoTotal' => $this->montoGravadoTotal,
'montoGravadoI1' => $this->montoGravadoI1,
'montoGravadoI2' => $this->montoGravadoI2,
'montoGravadoI3' => $this->montoGravadoI3,
'montoExento' => $this->montoExento,
'totalItbis' => $this->totalItbis,
'totalItbis1' => $this->totalItbis1,
'totalItbis2' => $this->totalItbis2,
'totalItbis3' => $this->totalItbis3,
'montoTotal' => $this->montoTotal,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
montoGravadoTotal: isset($data['montoGravadoTotal']) ? (float) $data['montoGravadoTotal'] : null,
montoGravadoI1: isset($data['montoGravadoI1']) ? (float) $data['montoGravadoI1'] : null,
montoGravadoI2: isset($data['montoGravadoI2']) ? (float) $data['montoGravadoI2'] : null,
montoGravadoI3: isset($data['montoGravadoI3']) ? (float) $data['montoGravadoI3'] : null,
montoExento: isset($data['montoExento']) ? (float) $data['montoExento'] : null,
totalItbis: isset($data['totalItbis']) ? (float) $data['totalItbis'] : null,
totalItbis1: isset($data['totalItbis1']) ? (float) $data['totalItbis1'] : null,
totalItbis2: isset($data['totalItbis2']) ? (float) $data['totalItbis2'] : null,
totalItbis3: isset($data['totalItbis3']) ? (float) $data['totalItbis3'] : null,
montoTotal: isset($data['montoTotal']) ? (float) $data['montoTotal'] : null,
);
}
}
/** Un descuento o recargo global (Sección D). */
final class EcfGlobalAdjustmentPayload implements JsonSerializable
{
public function __construct(
public readonly int $line,
public readonly ?string $kind = null,
public readonly ?int $affectsItbisRate = null,
public readonly ?float $amount = null,
public readonly ?bool $norm1007 = null,
public readonly ?string $description = null,
public readonly ?float $percentage = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'line' => $this->line,
'kind' => $this->kind,
'affectsItbisRate' => $this->affectsItbisRate,
'amount' => $this->amount,
'norm1007' => $this->norm1007,
'description' => $this->description,
'percentage' => $this->percentage,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
line: $data['line'],
kind: isset($data['kind']) ? $data['kind'] : null,
affectsItbisRate: isset($data['affectsItbisRate']) ? $data['affectsItbisRate'] : null,
amount: isset($data['amount']) ? (float) $data['amount'] : null,
norm1007: isset($data['norm1007']) ? $data['norm1007'] : null,
description: isset($data['description']) ? $data['description'] : null,
percentage: isset($data['percentage']) ? (float) $data['percentage'] : null,
);
}
}
final class EcfItemCodePayload implements JsonSerializable
{
public function __construct(
public readonly string $type,
public readonly string $value,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'type' => $this->type,
'value' => $this->value,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
type: $data['type'],
value: $data['value'],
);
}
}
/** Campos opcionales del `<Item>` — passthrough. La derivación del ISC desde `alcoholDegrees`/`referenceQuantity` es un slice posterior; hoy el cliente trae el monto final del ISC en `additionalTaxes` (el ISC específico ahí sí integra la base del ITBIS — ver EcfAdditionalTaxPayload). */
final class EcfLineDetailsPayload implements JsonSerializable
{
public function __construct(
public readonly ?float $referenceQuantity = null,
public readonly ?string $referenceUnit = null,
/** @var EcfSubquantityPayload[]|null */
public readonly ?array $subquantities = null,
public readonly ?float $alcoholDegrees = null,
public readonly ?float $referenceUnitPrice = null,
public readonly ?string $manufactureDate = null,
public readonly ?string $expiryDate = null,
public readonly ?EcfMiningPayload $mining = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'referenceQuantity' => $this->referenceQuantity,
'referenceUnit' => $this->referenceUnit,
'subquantities' => $this->subquantities,
'alcoholDegrees' => $this->alcoholDegrees,
'referenceUnitPrice' => $this->referenceUnitPrice,
'manufactureDate' => $this->manufactureDate,
'expiryDate' => $this->expiryDate,
'mining' => $this->mining,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
referenceQuantity: isset($data['referenceQuantity']) ? (float) $data['referenceQuantity'] : null,
referenceUnit: isset($data['referenceUnit']) ? $data['referenceUnit'] : null,
subquantities: isset($data['subquantities']) ? array_map(fn ($i) => EcfSubquantityPayload::fromArray($i), $data['subquantities']) : null,
alcoholDegrees: isset($data['alcoholDegrees']) ? (float) $data['alcoholDegrees'] : null,
referenceUnitPrice: isset($data['referenceUnitPrice']) ? (float) $data['referenceUnitPrice'] : null,
manufactureDate: isset($data['manufactureDate']) ? $data['manufactureDate'] : null,
expiryDate: isset($data['expiryDate']) ? $data['expiryDate'] : null,
mining: isset($data['mining']) ? EcfMiningPayload::fromArray($data['mining']) : null,
);
}
}
/** `<OtraMonedaDetalle>` — precio y montos de la línea en divisa (passthrough). */
final class EcfLineForeignCurrencyPayload implements JsonSerializable
{
public function __construct(
public readonly ?float $unitPrice = null,
public readonly ?float $discount = null,
public readonly ?float $surcharge = null,
public readonly ?float $lineAmount = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'unitPrice' => $this->unitPrice,
'discount' => $this->discount,
'surcharge' => $this->surcharge,
'lineAmount' => $this->lineAmount,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
unitPrice: isset($data['unitPrice']) ? (float) $data['unitPrice'] : null,
discount: isset($data['discount']) ? (float) $data['discount'] : null,
surcharge: isset($data['surcharge']) ? (float) $data['surcharge'] : null,
lineAmount: isset($data['lineAmount']) ? (float) $data['lineAmount'] : null,
);
}
}
/** Una línea de `<DetallesItems>`. */
final class EcfLinePayload implements JsonSerializable
{
public function __construct(
public readonly ?string $name = null,
public readonly ?string $kind = null,
public readonly ?float $quantity = null,
public readonly ?float $unitPrice = null,
public readonly ?int $itbisRate = null,
public readonly ?string $unitOfMeasure = null,
public readonly ?string $description = null,
public readonly ?float $discount = null,
public readonly ?float $surcharge = null,
public readonly ?bool $priceIncludesTax = null,
public readonly ?float $declaredAmount = null,
/** @var EcfItemCodePayload[]|null */
public readonly ?array $codes = null,
public readonly ?EcfLineRetentionPayload $retention = null,
/** @var EcfAdditionalTaxPayload[]|null */
public readonly ?array $additionalTaxes = null,
public readonly ?EcfLineForeignCurrencyPayload $foreignCurrency = null,
public readonly ?EcfLineDetailsPayload $details = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'name' => $this->name,
'kind' => $this->kind,
'quantity' => $this->quantity,
'unitPrice' => $this->unitPrice,
'itbisRate' => $this->itbisRate,
'unitOfMeasure' => $this->unitOfMeasure,
'description' => $this->description,
'discount' => $this->discount,
'surcharge' => $this->surcharge,
'priceIncludesTax' => $this->priceIncludesTax,
'declaredAmount' => $this->declaredAmount,
'codes' => $this->codes,
'retention' => $this->retention,
'additionalTaxes' => $this->additionalTaxes,
'foreignCurrency' => $this->foreignCurrency,
'details' => $this->details,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
name: isset($data['name']) ? $data['name'] : null,
kind: isset($data['kind']) ? $data['kind'] : null,
quantity: isset($data['quantity']) ? (float) $data['quantity'] : null,
unitPrice: isset($data['unitPrice']) ? (float) $data['unitPrice'] : null,
itbisRate: isset($data['itbisRate']) ? $data['itbisRate'] : null,
unitOfMeasure: isset($data['unitOfMeasure']) ? $data['unitOfMeasure'] : null,
description: isset($data['description']) ? $data['description'] : null,
discount: isset($data['discount']) ? (float) $data['discount'] : null,
surcharge: isset($data['surcharge']) ? (float) $data['surcharge'] : null,
priceIncludesTax: isset($data['priceIncludesTax']) ? $data['priceIncludesTax'] : null,
declaredAmount: isset($data['declaredAmount']) ? (float) $data['declaredAmount'] : null,
codes: isset($data['codes']) ? array_map(fn ($i) => EcfItemCodePayload::fromArray($i), $data['codes']) : null,
retention: isset($data['retention']) ? EcfLineRetentionPayload::fromArray($data['retention']) : null,
additionalTaxes: isset($data['additionalTaxes']) ? array_map(fn ($i) => EcfAdditionalTaxPayload::fromArray($i), $data['additionalTaxes']) : null,
foreignCurrency: isset($data['foreignCurrency']) ? EcfLineForeignCurrencyPayload::fromArray($data['foreignCurrency']) : null,
details: isset($data['details']) ? EcfLineDetailsPayload::fromArray($data['details']) : null,
);
}
}
/** Área `<Retencion>` de la línea (tipos 41 y 47). Los montos los calcula el cliente. */
final class EcfLineRetentionPayload implements JsonSerializable
{
public function __construct(
public readonly ?string $agent = null,
public readonly ?float $itbisWithheld = null,
public readonly ?float $isrWithheld = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'agent' => $this->agent,
'itbisWithheld' => $this->itbisWithheld,
'isrWithheld' => $this->isrWithheld,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
agent: isset($data['agent']) ? $data['agent'] : null,
itbisWithheld: isset($data['itbisWithheld']) ? (float) $data['itbisWithheld'] : null,
isrWithheld: isset($data['isrWithheld']) ? (float) $data['isrWithheld'] : null,
);
}
}
/** Enlaces (relativos) a los recursos del comprobante. */
final class EcfLinks implements JsonSerializable
{
public function __construct(
/** El comprobante y su estado. */
public readonly string $self,
/** El XML firmado (`<ECF>`). */
public readonly string $xml,
/** La Representación Impresa en PDF. */
public readonly string $representation,
/** El resumen firmado (`<RFCE>`); solo cuando `submitsRfce`. */
public readonly ?string $rfceXml = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'self' => $this->self,
'xml' => $this->xml,
'rfceXml' => $this->rfceXml,
'representation' => $this->representation,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
self: $data['self'],
xml: $data['xml'],
representation: $data['representation'],
rfceXml: isset($data['rfceXml']) ? $data['rfceXml'] : null,
);
}
}
/** `<Mineria>` — datos de liquidación minera (solo tipos 32/33/34/46). */
final class EcfMiningPayload implements JsonSerializable
{
public function __construct(
public readonly ?float $netWeightKilogram = null,
public readonly ?float $netWeightMining = null,
public readonly ?int $affiliationType = null,
public readonly ?int $settlement = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'netWeightKilogram' => $this->netWeightKilogram,
'netWeightMining' => $this->netWeightMining,
'affiliationType' => $this->affiliationType,
'settlement' => $this->settlement,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
netWeightKilogram: isset($data['netWeightKilogram']) ? (float) $data['netWeightKilogram'] : null,
netWeightMining: isset($data['netWeightMining']) ? (float) $data['netWeightMining'] : null,
affiliationType: isset($data['affiliationType']) ? $data['affiliationType'] : null,
settlement: isset($data['settlement']) ? $data['settlement'] : null,
);
}
}
/** Página de la RI (`<Paginacion>`). */
final class EcfPagePayload implements JsonSerializable
{
public function __construct(
public readonly ?int $number = null,
public readonly ?int $lineFrom = null,
public readonly ?int $lineTo = null,
public readonly ?float $montoGravadoTotal = null,
public readonly ?float $montoGravadoI1 = null,
public readonly ?float $montoGravadoI2 = null,
public readonly ?float $montoGravadoI3 = null,
public readonly ?float $montoExento = null,
public readonly ?float $totalItbis = null,
public readonly ?float $itbis1 = null,
public readonly ?float $itbis2 = null,
public readonly ?float $itbis3 = null,
public readonly ?float $montoImpuestoAdicional = null,
public readonly ?float $iscEspecifico = null,
public readonly ?float $otrosImpuestos = null,
public readonly ?float $amount = null,
public readonly ?float $nonInvoiceableAmount = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'number' => $this->number,
'lineFrom' => $this->lineFrom,
'lineTo' => $this->lineTo,
'montoGravadoTotal' => $this->montoGravadoTotal,
'montoGravadoI1' => $this->montoGravadoI1,
'montoGravadoI2' => $this->montoGravadoI2,
'montoGravadoI3' => $this->montoGravadoI3,
'montoExento' => $this->montoExento,
'totalItbis' => $this->totalItbis,
'itbis1' => $this->itbis1,
'itbis2' => $this->itbis2,
'itbis3' => $this->itbis3,
'montoImpuestoAdicional' => $this->montoImpuestoAdicional,
'iscEspecifico' => $this->iscEspecifico,
'otrosImpuestos' => $this->otrosImpuestos,
'amount' => $this->amount,
'nonInvoiceableAmount' => $this->nonInvoiceableAmount,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
number: isset($data['number']) ? $data['number'] : null,
lineFrom: isset($data['lineFrom']) ? $data['lineFrom'] : null,
lineTo: isset($data['lineTo']) ? $data['lineTo'] : null,
montoGravadoTotal: isset($data['montoGravadoTotal']) ? (float) $data['montoGravadoTotal'] : null,
montoGravadoI1: isset($data['montoGravadoI1']) ? (float) $data['montoGravadoI1'] : null,
montoGravadoI2: isset($data['montoGravadoI2']) ? (float) $data['montoGravadoI2'] : null,
montoGravadoI3: isset($data['montoGravadoI3']) ? (float) $data['montoGravadoI3'] : null,
montoExento: isset($data['montoExento']) ? (float) $data['montoExento'] : null,
totalItbis: isset($data['totalItbis']) ? (float) $data['totalItbis'] : null,
itbis1: isset($data['itbis1']) ? (float) $data['itbis1'] : null,
itbis2: isset($data['itbis2']) ? (float) $data['itbis2'] : null,
itbis3: isset($data['itbis3']) ? (float) $data['itbis3'] : null,
montoImpuestoAdicional: isset($data['montoImpuestoAdicional']) ? (float) $data['montoImpuestoAdicional'] : null,
iscEspecifico: isset($data['iscEspecifico']) ? (float) $data['iscEspecifico'] : null,
otrosImpuestos: isset($data['otrosImpuestos']) ? (float) $data['otrosImpuestos'] : null,
amount: isset($data['amount']) ? (float) $data['amount'] : null,
nonInvoiceableAmount: isset($data['nonInvoiceableAmount']) ? (float) $data['nonInvoiceableAmount'] : null,
);
}
}
/** Una forma de pago. `Type` y `Amount` son nullable para distinguir "no vino" de un valor: el validador exige ambos y la API nunca rellena uno por su cuenta. */
final class EcfPaymentMethodPayload implements JsonSerializable
{
public function __construct(
public readonly ?string $type = null,
public readonly ?float $amount = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'type' => $this->type,
'amount' => $this->amount,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
type: isset($data['type']) ? $data['type'] : null,
amount: isset($data['amount']) ? (float) $data['amount'] : null,
);
}
}
/** Bloque de pago del encabezado. */
final class EcfPaymentPayload implements JsonSerializable
{
public function __construct(
public readonly ?string $condition = null,
public readonly ?string $dueDate = null,
/** @var EcfPaymentMethodPayload[]|null */
public readonly ?array $methods = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'condition' => $this->condition,
'dueDate' => $this->dueDate,
'methods' => $this->methods,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
condition: isset($data['condition']) ? $data['condition'] : null,
dueDate: isset($data['dueDate']) ? $data['dueDate'] : null,
methods: isset($data['methods']) ? array_map(fn ($i) => EcfPaymentMethodPayload::fromArray($i), $data['methods']) : null,
);
}
}
/** Sección `<InformacionReferencia>` — Notas de Crédito/Débito y reemplazos. */
final class EcfReferencePayload implements JsonSerializable
{
public function __construct(
public readonly string $modifiedNcf,
public readonly string $modifiedNcfDate,
public readonly ?string $modificationCode = null,
public readonly ?string $otherIssuerRnc = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'modifiedNcf' => $this->modifiedNcf,
'modifiedNcfDate' => $this->modifiedNcfDate,
'modificationCode' => $this->modificationCode,
'otherIssuerRnc' => $this->otherIssuerRnc,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
modifiedNcf: $data['modifiedNcf'],
modifiedNcfDate: $data['modifiedNcfDate'],
modificationCode: isset($data['modificationCode']) ? $data['modificationCode'] : null,
otherIssuerRnc: isset($data['otherIssuerRnc']) ? $data['otherIssuerRnc'] : null,
);
}
}
/** Reintento automático de un comprobante tras un rechazo transitorio de la DGII. */
final class EcfRetry implements JsonSerializable
{
public function __construct(
/** Reenvíos automáticos que NovaFE ya hizo de este comprobante. */
public readonly int $count,
/** Tope de reenvíos automáticos. Al agotarse, el rechazo queda firme. */
public readonly int $max,
/** Cuándo es el próximo intento; `null` si no hay uno pendiente. */
public readonly ?string $nextAttemptAt = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'count' => $this->count,
'max' => $this->max,
'nextAttemptAt' => $this->nextAttemptAt,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
count: $data['count'],
max: $data['max'],
nextAttemptAt: isset($data['nextAttemptAt']) ? $data['nextAttemptAt'] : null,
);
}
}
/** Datos de embarque (`<InformacionesAdicionales>`). Passthrough. */
final class EcfShippingPayload implements JsonSerializable
{
public function __construct(
public readonly ?string $shipmentDate = null,
public readonly ?string $shipmentNumber = null,
public readonly ?string $containerNumber = null,
public readonly ?string $referenceNumber = null,
public readonly ?float $grossWeight = null,
public readonly ?float $netWeight = null,
public readonly ?string $grossWeightUnit = null,
public readonly ?string $netWeightUnit = null,
public readonly ?float $packageCount = null,
public readonly ?string $packageUnit = null,
public readonly ?float $volume = null,
public readonly ?string $volumeUnit = null,
public readonly ?EcfExportPayload $export = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'shipmentDate' => $this->shipmentDate,
'shipmentNumber' => $this->shipmentNumber,
'containerNumber' => $this->containerNumber,
'referenceNumber' => $this->referenceNumber,
'grossWeight' => $this->grossWeight,
'netWeight' => $this->netWeight,
'grossWeightUnit' => $this->grossWeightUnit,
'netWeightUnit' => $this->netWeightUnit,
'packageCount' => $this->packageCount,
'packageUnit' => $this->packageUnit,
'volume' => $this->volume,
'volumeUnit' => $this->volumeUnit,
'export' => $this->export,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
shipmentDate: isset($data['shipmentDate']) ? $data['shipmentDate'] : null,
shipmentNumber: isset($data['shipmentNumber']) ? $data['shipmentNumber'] : null,
containerNumber: isset($data['containerNumber']) ? $data['containerNumber'] : null,
referenceNumber: isset($data['referenceNumber']) ? $data['referenceNumber'] : null,
grossWeight: isset($data['grossWeight']) ? (float) $data['grossWeight'] : null,
netWeight: isset($data['netWeight']) ? (float) $data['netWeight'] : null,
grossWeightUnit: isset($data['grossWeightUnit']) ? $data['grossWeightUnit'] : null,
netWeightUnit: isset($data['netWeightUnit']) ? $data['netWeightUnit'] : null,
packageCount: isset($data['packageCount']) ? (float) $data['packageCount'] : null,
packageUnit: isset($data['packageUnit']) ? $data['packageUnit'] : null,
volume: isset($data['volume']) ? (float) $data['volume'] : null,
volumeUnit: isset($data['volumeUnit']) ? $data['volumeUnit'] : null,
export: isset($data['export']) ? EcfExportPayload::fromArray($data['export']) : null,
);
}
}
final class EcfSubquantityPayload implements JsonSerializable
{
public function __construct(
public readonly float $quantity,
public readonly string $unitCode,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'quantity' => $this->quantity,
'unitCode' => $this->unitCode,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
quantity: (float) $data['quantity'],
unitCode: $data['unitCode'],
);
}
}
/** Subtotal informativo para la RI (Sección C). No afecta la base imponible. */
final class EcfSubtotalPayload implements JsonSerializable
{
public function __construct(
public readonly ?int $number = null,
public readonly ?string $description = null,
public readonly ?int $order = null,
public readonly ?float $montoGravadoTotal = null,
public readonly ?float $montoGravadoI1 = null,
public readonly ?float $montoGravadoI2 = null,
public readonly ?float $montoGravadoI3 = null,
public readonly ?float $totalItbis = null,
public readonly ?float $itbis1 = null,
public readonly ?float $itbis2 = null,
public readonly ?float $itbis3 = null,
public readonly ?float $montoImpuestoAdicional = null,
public readonly ?float $montoExento = null,
public readonly ?float $amount = null,
public readonly ?int $lines = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'number' => $this->number,
'description' => $this->description,
'order' => $this->order,
'montoGravadoTotal' => $this->montoGravadoTotal,
'montoGravadoI1' => $this->montoGravadoI1,
'montoGravadoI2' => $this->montoGravadoI2,
'montoGravadoI3' => $this->montoGravadoI3,
'totalItbis' => $this->totalItbis,
'itbis1' => $this->itbis1,
'itbis2' => $this->itbis2,
'itbis3' => $this->itbis3,
'montoImpuestoAdicional' => $this->montoImpuestoAdicional,
'montoExento' => $this->montoExento,
'amount' => $this->amount,
'lines' => $this->lines,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
number: isset($data['number']) ? $data['number'] : null,
description: isset($data['description']) ? $data['description'] : null,
order: isset($data['order']) ? $data['order'] : null,
montoGravadoTotal: isset($data['montoGravadoTotal']) ? (float) $data['montoGravadoTotal'] : null,
montoGravadoI1: isset($data['montoGravadoI1']) ? (float) $data['montoGravadoI1'] : null,
montoGravadoI2: isset($data['montoGravadoI2']) ? (float) $data['montoGravadoI2'] : null,
montoGravadoI3: isset($data['montoGravadoI3']) ? (float) $data['montoGravadoI3'] : null,
totalItbis: isset($data['totalItbis']) ? (float) $data['totalItbis'] : null,
itbis1: isset($data['itbis1']) ? (float) $data['itbis1'] : null,
itbis2: isset($data['itbis2']) ? (float) $data['itbis2'] : null,
itbis3: isset($data['itbis3']) ? (float) $data['itbis3'] : null,
montoImpuestoAdicional: isset($data['montoImpuestoAdicional']) ? (float) $data['montoImpuestoAdicional'] : null,
montoExento: isset($data['montoExento']) ? (float) $data['montoExento'] : null,
amount: isset($data['amount']) ? (float) $data['amount'] : null,
lines: isset($data['lines']) ? $data['lines'] : null,
);
}
}
/** Fila del listado de comprobantes emitidos. */
final class EcfSummaryDto implements JsonSerializable
{
public function __construct(
public readonly string $id,
public readonly string $status,
public readonly string $encf,
public readonly int $type,
public readonly string $environment,
public readonly string $issueDate,
public readonly float $montoTotal,
public readonly string $createdAt,
public readonly ?string $buyerRnc = null,
public readonly ?string $buyerName = null,
/** Reintento automático (ver EcfRetry? EcfDto.Retry); `null` si no hubo ninguno. */
public readonly ?EcfRetry $retry = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'id' => $this->id,
'status' => $this->status,
'encf' => $this->encf,
'type' => $this->type,
'environment' => $this->environment,
'issueDate' => $this->issueDate,
'montoTotal' => $this->montoTotal,
'buyerRnc' => $this->buyerRnc,
'buyerName' => $this->buyerName,
'createdAt' => $this->createdAt,
'retry' => $this->retry,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
id: $data['id'],
status: $data['status'],
encf: $data['encf'],
type: $data['type'],
environment: $data['environment'],
issueDate: $data['issueDate'],
montoTotal: (float) $data['montoTotal'],
createdAt: $data['createdAt'],
buyerRnc: isset($data['buyerRnc']) ? $data['buyerRnc'] : null,
buyerName: isset($data['buyerName']) ? $data['buyerName'] : null,
retry: isset($data['retry']) ? EcfRetry::fromArray($data['retry']) : null,
);
}
}
/** Datos de transporte (`<Transporte>`). Passthrough. */
final class EcfTransportPayload implements JsonSerializable
{
public function __construct(
public readonly ?string $driver = null,
public readonly ?string $transportDocument = null,
public readonly ?string $vehicleId = null,
public readonly ?string $plate = null,
public readonly ?string $route = null,
public readonly ?string $zone = null,
public readonly ?string $deliveryNote = null,
public readonly ?string $via = null,
public readonly ?string $originCountry = null,
public readonly ?string $destinationAddress = null,
public readonly ?string $destinationCountry = null,
public readonly ?string $carrierRnc = null,
public readonly ?string $carrierName = null,
public readonly ?string $voyageNumber = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'driver' => $this->driver,
'transportDocument' => $this->transportDocument,
'vehicleId' => $this->vehicleId,
'plate' => $this->plate,
'route' => $this->route,
'zone' => $this->zone,
'deliveryNote' => $this->deliveryNote,
'via' => $this->via,
'originCountry' => $this->originCountry,
'destinationAddress' => $this->destinationAddress,
'destinationCountry' => $this->destinationCountry,
'carrierRnc' => $this->carrierRnc,
'carrierName' => $this->carrierName,
'voyageNumber' => $this->voyageNumber,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
driver: isset($data['driver']) ? $data['driver'] : null,
transportDocument: isset($data['transportDocument']) ? $data['transportDocument'] : null,
vehicleId: isset($data['vehicleId']) ? $data['vehicleId'] : null,
plate: isset($data['plate']) ? $data['plate'] : null,
route: isset($data['route']) ? $data['route'] : null,
zone: isset($data['zone']) ? $data['zone'] : null,
deliveryNote: isset($data['deliveryNote']) ? $data['deliveryNote'] : null,
via: isset($data['via']) ? $data['via'] : null,
originCountry: isset($data['originCountry']) ? $data['originCountry'] : null,
destinationAddress: isset($data['destinationAddress']) ? $data['destinationAddress'] : null,
destinationCountry: isset($data['destinationCountry']) ? $data['destinationCountry'] : null,
carrierRnc: isset($data['carrierRnc']) ? $data['carrierRnc'] : null,
carrierName: isset($data['carrierName']) ? $data['carrierName'] : null,
voyageNumber: isset($data['voyageNumber']) ? $data['voyageNumber'] : null,
);
}
}
/** Payload de emisión de un e-CF (`POST /api/v1/ecf`). Un solo objeto discriminado por int IssueEcfCommand.Type; el servidor asigna la secuencia, arma el bloque Emisor desde el perfil del tenant, calcula los totales, firma y persiste. */
final class IssueEcfCommand implements JsonSerializable
{
public function __construct(
/** Código DGII del tipo de e-CF: 31, 32, 33, 34, 41, 43, 44, 45, 46, 47. */
public readonly int $type,
/** @var EcfLinePayload[] */
public readonly array $lines,
/** e-NCF de 13 caracteres. Solo con `sequences.mode = external` (y obligatorio ahí); con `managed` se rechaza, NovaFE asigna el suyo. */
public readonly ?string $encf = null,
/** `<FechaVencimientoSecuencia>` del e-NCF. Con `sequences.mode = external` es obligatorio en los tipos que llevan vencimiento (todos menos 32 y 34) y no aplica en esos dos; en `managed` se rechaza. */
public readonly ?string $sequenceExpiresOn = null,
/** Fecha de emisión (calendario dominicano). Default: hoy. */
public readonly ?string $issueDate = null,
/** `<TipoIngresos>` — "01"…"06". Obligatorio en 31/32/33/34/44/45/46. */
public readonly ?string $incomeType = null,
/** `true` si los precios de las líneas ya traen el ITBIS incluido. */
public readonly ?bool $pricesIncludeTax = null,
/** `<IndicadorEnvioDiferido>` — solo contribuyentes autorizados. */
public readonly ?bool $deferredDelivery = null,
/** `<MontoNoFacturable>` — reembolsos, propina voluntaria. Puede ser negativo. */
public readonly ?float $nonInvoiceableAmount = null,
/** `<NumeroFacturaInterna>` — clave de dedup de negocio. */
public readonly ?string $internalNumber = null,
/** `<CodigoVendedor>`. */
public readonly ?string $sellerCode = null,
public readonly ?EcfAdditionalInfoPayload $additionalInfo = null,
public readonly ?EcfBuyerPayload $buyer = null,
public readonly ?EcfPaymentPayload $payment = null,
public readonly ?EcfReferencePayload $reference = null,
/** @var EcfGlobalAdjustmentPayload[]|null */
public readonly ?array $globalAdjustments = null,
public readonly ?EcfForeignCurrencyPayload $foreignCurrency = null,
public readonly ?EcfShippingPayload $shipping = null,
public readonly ?EcfTransportPayload $transport = null,
/** @var EcfSubtotalPayload[]|null */
public readonly ?array $subtotals = null,
/** @var EcfPagePayload[]|null */
public readonly ?array $pagination = null,
/** Totales declarados por el cliente (chequeo de tolerancia; nunca bloquea). */
public readonly ?EcfDeclaredTotalsPayload $declaredTotals = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'type' => $this->type,
'encf' => $this->encf,
'sequenceExpiresOn' => $this->sequenceExpiresOn,
'issueDate' => $this->issueDate,
'incomeType' => $this->incomeType,
'pricesIncludeTax' => $this->pricesIncludeTax,
'deferredDelivery' => $this->deferredDelivery,
'nonInvoiceableAmount' => $this->nonInvoiceableAmount,
'internalNumber' => $this->internalNumber,
'sellerCode' => $this->sellerCode,
'additionalInfo' => $this->additionalInfo,
'buyer' => $this->buyer,
'payment' => $this->payment,
'lines' => $this->lines,
'reference' => $this->reference,
'globalAdjustments' => $this->globalAdjustments,
'foreignCurrency' => $this->foreignCurrency,
'shipping' => $this->shipping,
'transport' => $this->transport,
'subtotals' => $this->subtotals,
'pagination' => $this->pagination,
'declaredTotals' => $this->declaredTotals,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
type: $data['type'],
lines: array_map(fn ($i) => EcfLinePayload::fromArray($i), $data['lines']),
encf: isset($data['encf']) ? $data['encf'] : null,
sequenceExpiresOn: isset($data['sequenceExpiresOn']) ? $data['sequenceExpiresOn'] : null,
issueDate: isset($data['issueDate']) ? $data['issueDate'] : null,
incomeType: isset($data['incomeType']) ? $data['incomeType'] : null,
pricesIncludeTax: isset($data['pricesIncludeTax']) ? $data['pricesIncludeTax'] : null,
deferredDelivery: isset($data['deferredDelivery']) ? $data['deferredDelivery'] : null,
nonInvoiceableAmount: isset($data['nonInvoiceableAmount']) ? (float) $data['nonInvoiceableAmount'] : null,
internalNumber: isset($data['internalNumber']) ? $data['internalNumber'] : null,
sellerCode: isset($data['sellerCode']) ? $data['sellerCode'] : null,
additionalInfo: isset($data['additionalInfo']) ? EcfAdditionalInfoPayload::fromArray($data['additionalInfo']) : null,
buyer: isset($data['buyer']) ? EcfBuyerPayload::fromArray($data['buyer']) : null,
payment: isset($data['payment']) ? EcfPaymentPayload::fromArray($data['payment']) : null,
reference: isset($data['reference']) ? EcfReferencePayload::fromArray($data['reference']) : null,
globalAdjustments: isset($data['globalAdjustments']) ? array_map(fn ($i) => EcfGlobalAdjustmentPayload::fromArray($i), $data['globalAdjustments']) : null,
foreignCurrency: isset($data['foreignCurrency']) ? EcfForeignCurrencyPayload::fromArray($data['foreignCurrency']) : null,
shipping: isset($data['shipping']) ? EcfShippingPayload::fromArray($data['shipping']) : null,
transport: isset($data['transport']) ? EcfTransportPayload::fromArray($data['transport']) : null,
subtotals: isset($data['subtotals']) ? array_map(fn ($i) => EcfSubtotalPayload::fromArray($i), $data['subtotals']) : null,
pagination: isset($data['pagination']) ? array_map(fn ($i) => EcfPagePayload::fromArray($i), $data['pagination']) : null,
declaredTotals: isset($data['declaredTotals']) ? EcfDeclaredTotalsPayload::fromArray($data['declaredTotals']) : null,
);
}
}
final class PagedResultOfEcfSummaryDto implements JsonSerializable
{
public function __construct(
/** @var EcfSummaryDto[] */
public readonly array $items,
public readonly int $totalCount,
public readonly int $page,
public readonly int $pageSize,
public readonly ?int $totalPages = null,
public readonly ?bool $hasNextPage = null,
public readonly ?bool $hasPreviousPage = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'items' => $this->items,
'totalCount' => $this->totalCount,
'page' => $this->page,
'pageSize' => $this->pageSize,
'totalPages' => $this->totalPages,
'hasNextPage' => $this->hasNextPage,
'hasPreviousPage' => $this->hasPreviousPage,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
items: array_map(fn ($i) => EcfSummaryDto::fromArray($i), $data['items']),
totalCount: $data['totalCount'],
page: $data['page'],
pageSize: $data['pageSize'],
totalPages: isset($data['totalPages']) ? $data['totalPages'] : null,
hasNextPage: isset($data['hasNextPage']) ? $data['hasNextPage'] : null,
hasPreviousPage: isset($data['hasPreviousPage']) ? $data['hasPreviousPage'] : null,
);
}
}
final class RepresentationLayout implements JsonSerializable
{
public function __construct(
) {
}
public function jsonSerialize(): array
{
return array_filter([
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
);
}
}
/** Un error o aviso de `POST /api/v1/ecf/validate`. */
final class ValidateEcfIssueDto implements JsonSerializable
{
public function __construct(
/** Código estable, apto para lógica del cliente. */
public readonly string $code,
/** Descripción lista para mostrar. */
public readonly string $message,
/** Campo del payload al que se refiere (p. ej. `Lines[0].Quantity`); `null` si es de todo el comprobante. */
public readonly ?string $field = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'field' => $this->field,
'code' => $this->code,
'message' => $this->message,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
code: $data['code'],
message: $data['message'],
field: isset($data['field']) ? $data['field'] : null,
);
}
}
/** Vista previa de `POST /api/v1/ecf/validate` para un payload válido: pasó la matriz de validación por tipo (Módulo 2) y el cálculo de totales (Módulo 6) sin asignar una secuencia real, firmar ni persistir nada. */
final class ValidateEcfPreviewDto implements JsonSerializable
{
public function __construct(
public readonly int $type,
public readonly string $typeName,
/** Placeholder (`E{tipo}0000000000`) — nunca un e-NCF real, no se asignó ninguna secuencia para construir este documento. */
public readonly string $sampleEncf,
public readonly string $issueDate,
public readonly float $montoGravadoTotal,
public readonly float $montoExento,
public readonly float $totalItbis,
public readonly float $montoImpuestoAdicional,
public readonly float $montoTotal,
/** Los totales quedarían fuera de la tolerancia de cuadratura (RF-06.6) — la DGII probablemente aceptaría el comprobante de forma condicional si se emite así. */
public readonly bool $expectConditionalAcceptance,
/** Estimado (31-dic del año siguiente a DateOnly ValidateEcfPreviewDto.IssueDate) cuando el tipo lo lleva; no viene de una secuencia real. `null` en los tipos que no lo llevan (32, 34). */
public readonly ?string $sequenceExpiresOnEstimate = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'type' => $this->type,
'typeName' => $this->typeName,
'sampleEncf' => $this->sampleEncf,
'issueDate' => $this->issueDate,
'sequenceExpiresOnEstimate' => $this->sequenceExpiresOnEstimate,
'montoGravadoTotal' => $this->montoGravadoTotal,
'montoExento' => $this->montoExento,
'totalItbis' => $this->totalItbis,
'montoImpuestoAdicional' => $this->montoImpuestoAdicional,
'montoTotal' => $this->montoTotal,
'expectConditionalAcceptance' => $this->expectConditionalAcceptance,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
type: $data['type'],
typeName: $data['typeName'],
sampleEncf: $data['sampleEncf'],
issueDate: $data['issueDate'],
montoGravadoTotal: (float) $data['montoGravadoTotal'],
montoExento: (float) $data['montoExento'],
totalItbis: (float) $data['totalItbis'],
montoImpuestoAdicional: (float) $data['montoImpuestoAdicional'],
montoTotal: (float) $data['montoTotal'],
expectConditionalAcceptance: $data['expectConditionalAcceptance'],
sequenceExpiresOnEstimate: isset($data['sequenceExpiresOnEstimate']) ? $data['sequenceExpiresOnEstimate'] : null,
);
}
}
/** Resultado de `POST /api/v1/ecf/validate`. Siempre llega con `200` cuando el cuerpo se pudo leer y el contribuyente puede validar: bool ValidateEcfResultDto.Valid dice si el comprobante pasaría la validación de forma y la matriz estructural y fiscal por tipo (Módulo 2 + 6) que corre `POST /ecf`. No asigna una secuencia real, no firma y no persiste nada. Lo que impide validar (sin autenticar, perfil del emisor sin configurar) sigue siendo un error HTTP. */
final class ValidateEcfResultDto implements JsonSerializable
{
public function __construct(
/** `true` si el comprobante se podría emitir tal cual en cuanto a su contenido. */
public readonly bool $valid,
/** Resumen en una frase, listo para mostrar. */
public readonly string $message,
/** @var ValidateEcfIssueDto[] Todos los problemas encontrados, no solo el primero. Vacío si bool ValidateEcfResultDto.Valid. */
public readonly array $errors,
/** @var ValidateEcfIssueDto[] Avisos que no impiden emitir (p. ej. aceptación condicional probable). */
public readonly array $warnings,
/** La vista previa de lo que se emitiría; `null` si el comprobante no es válido. */
public readonly ?ValidateEcfPreviewDto $preview = null,
) {
}
public function jsonSerialize(): array
{
return array_filter([
'valid' => $this->valid,
'message' => $this->message,
'errors' => $this->errors,
'warnings' => $this->warnings,
'preview' => $this->preview,
], fn ($v) => $v !== null);
}
public static function fromArray(array $data): self
{
return new self(
valid: $data['valid'],
message: $data['message'],
errors: array_map(fn ($i) => ValidateEcfIssueDto::fromArray($i), $data['errors']),
warnings: array_map(fn ($i) => ValidateEcfIssueDto::fromArray($i), $data['warnings']),
preview: isset($data['preview']) ? ValidateEcfPreviewDto::fromArray($data['preview']) : null,
);
}
}