SDK y modelos: PHP
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.
Descargar
Sección titulada «Descargar»- Ecf.php: emisión de e-CF, con sus tipos anidados.
- novafe-models-php.zip: todas las áreas de la API.
- documento OpenAPI: el contrato completo, para generar tus propios modelos.
Ejemplo: emitir un comprobante
Sección titulada «Ejemplo: emitir un comprobante»<?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;Modelos de emisión
Sección titulada «Modelos de emisión»Todo el contenido de Ecf.php, listo para copiar. Incluye IssueEcfCommand (el cuerpo de POST /ecf), EcfDto (la respuesta) y sus tipos anidados.
<?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, ); }}