From 8444ec144b047b72ba2ce7750cedfb35f6e646ed Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Tue, 7 Dec 2021 21:27:39 -0600 Subject: [PATCH 1/4] Add internal development tool elements-maker.php --- composer.json | 1 + development/BaseCliApplication.php | 57 ++++++++++++ development/ElementsMaker/Dictionary.php | 38 ++++++++ development/ElementsMaker/ElementsMaker.php | 88 ++++++++++++++++++ development/ElementsMaker/Specifications.php | 51 +++++++++++ .../ElementsMaker/SpecificationsReader.php | 67 ++++++++++++++ development/ElementsMaker/Structure.php | 91 +++++++++++++++++++ .../specifications/cartaporte20.json | 51 +++++++++++ .../templates/child-multiple.template | 14 +++ .../templates/child-single.template | 11 +++ .../ElementsMaker/templates/element.template | 14 +++ .../templates/get-children-order.template | 4 + .../templates/get-fixed-attributes.template | 9 ++ development/README.md | 28 ++++++ development/bin/elements-maker.php | 38 ++++++++ 15 files changed, 562 insertions(+) create mode 100644 development/BaseCliApplication.php create mode 100644 development/ElementsMaker/Dictionary.php create mode 100644 development/ElementsMaker/ElementsMaker.php create mode 100644 development/ElementsMaker/Specifications.php create mode 100644 development/ElementsMaker/SpecificationsReader.php create mode 100644 development/ElementsMaker/Structure.php create mode 100644 development/ElementsMaker/specifications/cartaporte20.json create mode 100644 development/ElementsMaker/templates/child-multiple.template create mode 100644 development/ElementsMaker/templates/child-single.template create mode 100644 development/ElementsMaker/templates/element.template create mode 100644 development/ElementsMaker/templates/get-children-order.template create mode 100644 development/ElementsMaker/templates/get-fixed-attributes.template create mode 100644 development/README.md create mode 100644 development/bin/elements-maker.php diff --git a/composer.json b/composer.json index d120ad34..4ad091a7 100644 --- a/composer.json +++ b/composer.json @@ -55,6 +55,7 @@ }, "autoload-dev": { "psr-4": { + "CfdiUtils\\Development\\": "development/", "CfdiUtilsTests\\": "tests/CfdiUtilsTests/" } }, diff --git a/development/BaseCliApplication.php b/development/BaseCliApplication.php new file mode 100644 index 00000000..81e75676 --- /dev/null +++ b/development/BaseCliApplication.php @@ -0,0 +1,57 @@ +command = $command; + $this->arguments = $arguments; + } + + public function __invoke(): int + { + if ([] !== array_intersect(['-h', '--help'], $this->arguments)) { + $this->printHelp(); + return 0; + } + + try { + return $this->execute(); + } catch (\Throwable $exception) { + file_put_contents('php://stderr', $exception->getMessage() . PHP_EOL, FILE_APPEND); + // file_put_contents('php://stderr', get_class($exception) . ': ' . $exception->getMessage() . PHP_EOL, FILE_APPEND); + // file_put_contents('php://stderr', $exception->getTraceAsString() . PHP_EOL, FILE_APPEND); + return min(1, $exception->getCode()); + } + } + + public function getCommand(): string + { + return $this->command; + } + + /** @return string[] */ + public function getArguments(): array + { + return $this->arguments; + } + + public function getArgument(int $index): string + { + return $this->arguments[$index] ?? ''; + } +} diff --git a/development/ElementsMaker/Dictionary.php b/development/ElementsMaker/Dictionary.php new file mode 100644 index 00000000..dc94f6af --- /dev/null +++ b/development/ElementsMaker/Dictionary.php @@ -0,0 +1,38 @@ + */ + private $values; + + /** @param array $values */ + public function __construct(array $values) + { + $this->values = $values; + } + + public function get(string $key): string + { + return $this->values[$key] ?? ''; + } + + public function with(string $key, string $value): self + { + return new self(array_merge($this->values, [$key => $value])); + } + + /** @return array $values */ + public function getValues(): array + { + return $this->values; + } + + public function interpolate(string $subject): string + { + return strtr($subject, $this->values); + } +} diff --git a/development/ElementsMaker/ElementsMaker.php b/development/ElementsMaker/ElementsMaker.php new file mode 100644 index 00000000..31a702b6 --- /dev/null +++ b/development/ElementsMaker/ElementsMaker.php @@ -0,0 +1,88 @@ + */ + private $templates = []; + + public function __construct(Specifications $specs, string $outputDir) + { + $this->specs = $specs; + $this->outputDir = $outputDir; + } + + public static function make(string $specFile, string $outputDir): self + { + return new self(Specifications::makeFromFile($specFile), $outputDir); + } + + public function write(): void + { + $this->createElement($this->specs->getStructure(), $this->specs->getDictionary(), true); + } + + public function createElement(Structure $structure, Dictionary $dictionary, bool $isRoot = false): void + { + $prefix = $dictionary->get('#prefix#'); + $dictionary = $dictionary->with('#element-name#', $structure->getName()); + $sectionsContent = []; + $orderElements = $structure->getChildrenNames($prefix . ':'); + + if (count($orderElements) > 1) { + $sectionsContent[] = $this->template( + 'get-children-order', + new Dictionary(['#elements#' => $this->elementsToString($orderElements)]) + ); + } + + if ($isRoot) { + $sectionsContent[] = $this->template('get-fixed-attributes', $dictionary); + } + + /** @var Structure $child */ + foreach ($structure as $child) { + $childTemplate = ($child->isMultiple()) ? 'child-multiple' : 'child-single'; + $sectionsContent[] = $this->template($childTemplate, new Dictionary(['#child-name#' => $child->getName()])); + $this->createElement($child, $dictionary); + } + + $contents = $this->template('element', $dictionary->with('#sections#', implode('', $sectionsContent))); + $outputFile = $this->buildOutputFile($structure->getName()); + file_put_contents($outputFile, $contents); + } + + private function template(string $templateName, Dictionary $dictionary): string + { + if (! isset($this->templates[$templateName])) { + $fileName = __DIR__ . '/templates/' . $templateName . '.template'; + $this->templates[$templateName] = file_get_contents($fileName) ?: ''; + } + + return $dictionary->interpolate($this->templates[$templateName]); + } + + private function buildOutputFile(string $elementName): string + { + return $this->outputDir . DIRECTORY_SEPARATOR . $elementName . '.php'; + } + + /** @param string[] $array */ + private function elementsToString(array $array): string + { + $parts = []; + foreach ($array as $value) { + $parts[] = var_export($value, true); + } + return "[\n" . implode(",\n", $parts) . ']'; + } +} diff --git a/development/ElementsMaker/Specifications.php b/development/ElementsMaker/Specifications.php new file mode 100644 index 00000000..780c22a5 --- /dev/null +++ b/development/ElementsMaker/Specifications.php @@ -0,0 +1,51 @@ +structure = $structure; + $this->dictionary = $dictionary; + } + + public static function makeFromFile(string $specFile): self + { + $specFileReader = SpecificationsReader::fromFile($specFile); + + $structure = Structure::makeFromStdClass( + $specFileReader->keyAsString('root-element'), + $specFileReader->keyAsStdClass('structure') + ); + + $dictionary = new Dictionary([ + '#php-namespace#' => $specFileReader->keyAsString('php-namespace'), + '#prefix#' => $specFileReader->keyAsString('prefix'), + '#xml-namespace#' => $specFileReader->keyAsString('xml-namespace'), + '#xml-schemalocation#' => $specFileReader->keyAsString('xml-schemalocation'), + '#version-attribute#' => $specFileReader->keyAsString('version-attribute'), + '#version-value#' => $specFileReader->keyAsString('version-value'), + ]); + + return new self($structure, $dictionary); + } + + public function getStructure(): Structure + { + return $this->structure; + } + + public function getDictionary(): Dictionary + { + return $this->dictionary; + } +} diff --git a/development/ElementsMaker/SpecificationsReader.php b/development/ElementsMaker/SpecificationsReader.php new file mode 100644 index 00000000..74fb4bf7 --- /dev/null +++ b/development/ElementsMaker/SpecificationsReader.php @@ -0,0 +1,67 @@ +data = $data; + } + + public static function fromFile(string $specFile): self + { + if (! file_exists($specFile)) { + throw new RuntimeException("Specification file '$specFile' does not exists"); + } + $specContents = file_get_contents($specFile); + if (false === $specContents) { + throw new RuntimeException("Unable to read $specFile"); + } + return self::fromJsonString($specContents); + } + + public static function fromJsonString(string $specContents): self + { + try { + $data = json_decode($specContents, false, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + throw new RuntimeException('Unable to parse the JSON specification', 0, $exception); + } + if (! $data instanceof stdClass) { + throw new RuntimeException('The JSON specification does not contains a valid root object'); + } + return new self($data); + } + + public function keyAsString(string $name): string + { + if (! isset($this->data->{$name})) { + return ''; + } + if (! is_string($this->data->{$name})) { + return ''; + } + return $this->data->{$name}; + } + + public function keyAsStdClass(string $name): stdClass + { + if (! isset($this->data->{$name})) { + return (object) []; + } + if (! $this->data->{$name} instanceof stdClass) { + return (object) []; + } + return $this->data->{$name}; + } +} diff --git a/development/ElementsMaker/Structure.php b/development/ElementsMaker/Structure.php new file mode 100644 index 00000000..48143a91 --- /dev/null +++ b/development/ElementsMaker/Structure.php @@ -0,0 +1,91 @@ +name = $name; + $this->multiple = $multiple; + $this->children = $children; + } + + public static function makeFromStdClass(string $name, stdClass $data): self + { + $multiple = false; + if (isset($data->{'multiple'}) && is_bool($data->{'multiple'})) { + $multiple = $data->{'multiple'}; + } + $children = []; + + foreach (get_object_vars($data) as $key => $value) { + if ($value instanceof stdClass) { + $children[] = self::makeFromStdClass($key, $value); + } + } + + return new self($name, $multiple, ...$children); + } + + public function getName(): string + { + return $this->name; + } + + public function isMultiple(): bool + { + return $this->multiple; + } + + public function getChildren(): array + { + return $this->children; + } + + /** @return Traversable */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->children); + } + + /** @return string[] */ + public function getChildrenNames(string $prefix): array + { + return array_unique( + array_map( + function (self $structure) use ($prefix): string { + return $prefix . $structure->getName(); + }, + $this->children + ) + ); + } + + public function count(): int + { + return count($this->children); + } +} diff --git a/development/ElementsMaker/specifications/cartaporte20.json b/development/ElementsMaker/specifications/cartaporte20.json new file mode 100644 index 00000000..ed649d41 --- /dev/null +++ b/development/ElementsMaker/specifications/cartaporte20.json @@ -0,0 +1,51 @@ +{ + "php-namespace": "CfdiUtils\\Elements\\CartaPorte20", + "prefix": "cartaporte20", + "xml-namespace": "http://www.sat.gob.mx/cartaporte", + "xml-schemalocation": "http://www.sat.gob.mx/sitio_internet/cfd/CartaPorte/CartaPorte20.xsd", + "version-attribute": "Version", + "version-value": "2.0", + "root-element": "CartaPorte", + "structure": { + "Ubicaciones": { + "Ubicacion": { + "multiple": true, + "Domicilio": {} + } + }, + "Mercancias": { + "Mercancia": { + "multiple": true, + "Pedimentos": {"multiple": true}, + "GuiasIdentificacion": {"multiple": true}, + "CantidadTransporta": {"multiple": true}, + "DetalleMercancia": {} + }, + "Autotransporte": { + "IdentificacionVehicular": {}, + "Seguros": {}, + "Remolques": { + "Remolque": {"multiple": true} + } + }, + "TransporteMaritimo": { + "Contenedor": {"multiple": true} + }, + "TransporteAereo": {}, + "TransporteFerroviario": { + "DerechosDePaso": {"multiple": true}, + "Carro": { + "multiple": true, + "Contenedor": {"multiple": true} + } + } + }, + "FiguraTransporte": { + "TiposFigura": { + "multiple": true, + "PartesTransporte": {"multiple": true}, + "Domicilio": {} + } + } + } +} diff --git a/development/ElementsMaker/templates/child-multiple.template b/development/ElementsMaker/templates/child-multiple.template new file mode 100644 index 00000000..33fcc1cc --- /dev/null +++ b/development/ElementsMaker/templates/child-multiple.template @@ -0,0 +1,14 @@ + public function add#child-name#(array $attributes = []): #child-name# + { + $subject = new #child-name#($attributes); + $this->addChild($subject); + return $subject; + } + + public function multi#child-name#(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->add#child-name#($attributes); + } + return $this; + } diff --git a/development/ElementsMaker/templates/child-single.template b/development/ElementsMaker/templates/child-single.template new file mode 100644 index 00000000..9469b463 --- /dev/null +++ b/development/ElementsMaker/templates/child-single.template @@ -0,0 +1,11 @@ + public function get#child-name#(): #child-name# + { + return $this->helperGetOrAdd(new #child-name#()); + } + + public function add#child-name#(array $attributes = []): #child-name# + { + $subject = $this->get#child-name#(); + $subject->addAttributes($attributes); + return $subject; + } diff --git a/development/ElementsMaker/templates/element.template b/development/ElementsMaker/templates/element.template new file mode 100644 index 00000000..5a1de9d5 --- /dev/null +++ b/development/ElementsMaker/templates/element.template @@ -0,0 +1,14 @@ + '#xml-namespace#', + 'xsi:schemaLocation' => '#xml-namespace#' + . ' #xml-schemalocation#', + '#version-attribute#' => '#version-value#', + ]; + } diff --git a/development/README.md b/development/README.md new file mode 100644 index 00000000..f5278bb7 --- /dev/null +++ b/development/README.md @@ -0,0 +1,28 @@ +# Development tools + +En este espacio se encuentran algunas herramientas de desarrollo internas. + +**Importante:** Estas herramientas no están diseñadas para trabajar fuera de esta librería ni se deben +incluir en el paquete distribuible. + +## ElementsMarker + +Esta herramienta fabrica elementos usando un archivo de especificación. + +```php +php development/bin/elements-maker.php specification-file output-directory +``` + +Para un ejemplo del contenido del argumento `specification-file` se puede ver el archivo +`development/ElementsMaker/specifications/cartaporte20.json`. + +El argumento `output-directory` es donde se generarán los archivos de tipo `Element` de acuerdo a la especificación. + +El siguiente es el ejemplo de cómo se crearon los elementos de `CartaPorte20`. + +```shell +rm -rf src/CfdiUtils/Elements/CartaPorte20 +mkdir -p src/CfdiUtils/Elements/CartaPorte20 +php development/bin/elements-maker.php development/ElementsMaker/specifications/cartaporte20.json src/CfdiUtils/Elements/CartaPorte20/ +composer dev:fix-style +``` diff --git a/development/bin/elements-maker.php b/development/bin/elements-maker.php new file mode 100644 index 00000000..a8d81c24 --- /dev/null +++ b/development/bin/elements-maker.php @@ -0,0 +1,38 @@ +getCommand()); + echo implode(PHP_EOL, [ + "$command - Creación de elementos a partir de una especificación.", + 'Sintaxis: ', + "$command specification-file output-directory", + ' specification-file: the location of the specification file', + ' output-directory: the location where files should be written', + '', + ]); + } + + public function execute(): int + { + $specFile = $this->getArgument(0); + if ('' === $specFile) { + throw new RuntimeException('Argument specification-file not set'); + } + + $outputDir = $this->getArgument(1); + if ('' === $outputDir) { + throw new RuntimeException('Argument output-directory not set'); + } + + $elementsMaker = ElementsMaker::make($specFile, $outputDir); + $elementsMaker->write(); + return 0; + } +})); From b88ff77b7a4ad040a0a4f4550eb39f2caca012a1 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 17 Dec 2021 12:03:00 -0600 Subject: [PATCH 2/4] Add Carta Porte 2.0 --- .../Elements/CartaPorte20/Autotransporte.php | 57 +++++ .../CartaPorte20/CantidadTransporta.php | 13 + src/CfdiUtils/Elements/CartaPorte20/Carro.php | 28 +++ .../Elements/CartaPorte20/CartaPorte.php | 67 +++++ .../Elements/CartaPorte20/Contenedor.php | 13 + .../Elements/CartaPorte20/DerechosDePaso.php | 13 + .../CartaPorte20/DetalleMercancia.php | 13 + .../Elements/CartaPorte20/Domicilio.php | 13 + .../CartaPorte20/FiguraTransporte.php | 28 +++ .../CartaPorte20/GuiasIdentificacion.php | 13 + .../CartaPorte20/IdentificacionVehicular.php | 13 + .../Elements/CartaPorte20/Mercancia.php | 79 ++++++ .../Elements/CartaPorte20/Mercancias.php | 86 +++++++ .../CartaPorte20/PartesTransporte.php | 13 + .../Elements/CartaPorte20/Pedimentos.php | 13 + .../Elements/CartaPorte20/Remolque.php | 13 + .../Elements/CartaPorte20/Remolques.php | 28 +++ .../Elements/CartaPorte20/Seguros.php | 13 + .../Elements/CartaPorte20/TiposFigura.php | 47 ++++ .../Elements/CartaPorte20/TransporteAereo.php | 13 + .../CartaPorte20/TransporteFerroviario.php | 50 ++++ .../CartaPorte20/TransporteMaritimo.php | 28 +++ .../Elements/CartaPorte20/Ubicacion.php | 25 ++ .../Elements/CartaPorte20/Ubicaciones.php | 28 +++ .../Elements/CartaPorte20/CartaPorteTest.php | 238 ++++++++++++++++++ .../Elements/ElementTestCase.php | 124 +++++++++ 26 files changed, 1069 insertions(+) create mode 100644 src/CfdiUtils/Elements/CartaPorte20/Autotransporte.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/CantidadTransporta.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/Carro.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/CartaPorte.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/Contenedor.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/DerechosDePaso.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/DetalleMercancia.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/Domicilio.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/FiguraTransporte.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/GuiasIdentificacion.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/IdentificacionVehicular.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/Mercancia.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/Mercancias.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/PartesTransporte.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/Pedimentos.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/Remolque.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/Remolques.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/Seguros.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/TiposFigura.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/TransporteAereo.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/TransporteFerroviario.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/TransporteMaritimo.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/Ubicacion.php create mode 100644 src/CfdiUtils/Elements/CartaPorte20/Ubicaciones.php create mode 100644 tests/CfdiUtilsTests/Elements/CartaPorte20/CartaPorteTest.php create mode 100644 tests/CfdiUtilsTests/Elements/ElementTestCase.php diff --git a/src/CfdiUtils/Elements/CartaPorte20/Autotransporte.php b/src/CfdiUtils/Elements/CartaPorte20/Autotransporte.php new file mode 100644 index 00000000..76be9eed --- /dev/null +++ b/src/CfdiUtils/Elements/CartaPorte20/Autotransporte.php @@ -0,0 +1,57 @@ +helperGetOrAdd(new IdentificacionVehicular()); + } + + public function addIdentificacionVehicular(array $attributes = []): IdentificacionVehicular + { + $subject = $this->getIdentificacionVehicular(); + $subject->addAttributes($attributes); + return $subject; + } + + public function getSeguros(): Seguros + { + return $this->helperGetOrAdd(new Seguros()); + } + + public function addSeguros(array $attributes = []): Seguros + { + $subject = $this->getSeguros(); + $subject->addAttributes($attributes); + return $subject; + } + + public function getRemolques(): Remolques + { + return $this->helperGetOrAdd(new Remolques()); + } + + public function addRemolques(array $attributes = []): Remolques + { + $subject = $this->getRemolques(); + $subject->addAttributes($attributes); + return $subject; + } +} diff --git a/src/CfdiUtils/Elements/CartaPorte20/CantidadTransporta.php b/src/CfdiUtils/Elements/CartaPorte20/CantidadTransporta.php new file mode 100644 index 00000000..672915be --- /dev/null +++ b/src/CfdiUtils/Elements/CartaPorte20/CantidadTransporta.php @@ -0,0 +1,13 @@ +addChild($subject); + return $subject; + } + + public function multiContenedor(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addContenedor($attributes); + } + return $this; + } +} diff --git a/src/CfdiUtils/Elements/CartaPorte20/CartaPorte.php b/src/CfdiUtils/Elements/CartaPorte20/CartaPorte.php new file mode 100644 index 00000000..52246a25 --- /dev/null +++ b/src/CfdiUtils/Elements/CartaPorte20/CartaPorte.php @@ -0,0 +1,67 @@ + 'http://www.sat.gob.mx/cartaporte', + 'xsi:schemaLocation' => 'http://www.sat.gob.mx/cartaporte' + . ' http://www.sat.gob.mx/sitio_internet/cfd/CartaPorte/CartaPorte20.xsd', + 'Version' => '2.0', + ]; + } + + public function getUbicaciones(): Ubicaciones + { + return $this->helperGetOrAdd(new Ubicaciones()); + } + + public function addUbicaciones(array $attributes = []): Ubicaciones + { + $subject = $this->getUbicaciones(); + $subject->addAttributes($attributes); + return $subject; + } + + public function getMercancias(): Mercancias + { + return $this->helperGetOrAdd(new Mercancias()); + } + + public function addMercancias(array $attributes = []): Mercancias + { + $subject = $this->getMercancias(); + $subject->addAttributes($attributes); + return $subject; + } + + public function getFiguraTransporte(): FiguraTransporte + { + return $this->helperGetOrAdd(new FiguraTransporte()); + } + + public function addFiguraTransporte(array $attributes = []): FiguraTransporte + { + $subject = $this->getFiguraTransporte(); + $subject->addAttributes($attributes); + return $subject; + } +} diff --git a/src/CfdiUtils/Elements/CartaPorte20/Contenedor.php b/src/CfdiUtils/Elements/CartaPorte20/Contenedor.php new file mode 100644 index 00000000..28628f53 --- /dev/null +++ b/src/CfdiUtils/Elements/CartaPorte20/Contenedor.php @@ -0,0 +1,13 @@ +addChild($subject); + return $subject; + } + + public function multiTiposFigura(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addTiposFigura($attributes); + } + return $this; + } +} diff --git a/src/CfdiUtils/Elements/CartaPorte20/GuiasIdentificacion.php b/src/CfdiUtils/Elements/CartaPorte20/GuiasIdentificacion.php new file mode 100644 index 00000000..2deb5c39 --- /dev/null +++ b/src/CfdiUtils/Elements/CartaPorte20/GuiasIdentificacion.php @@ -0,0 +1,13 @@ +addChild($subject); + return $subject; + } + + public function multiPedimentos(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addPedimentos($attributes); + } + return $this; + } + + public function addGuiasIdentificacion(array $attributes = []): GuiasIdentificacion + { + $subject = new GuiasIdentificacion($attributes); + $this->addChild($subject); + return $subject; + } + + public function multiGuiasIdentificacion(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addGuiasIdentificacion($attributes); + } + return $this; + } + + public function addCantidadTransporta(array $attributes = []): CantidadTransporta + { + $subject = new CantidadTransporta($attributes); + $this->addChild($subject); + return $subject; + } + + public function multiCantidadTransporta(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addCantidadTransporta($attributes); + } + return $this; + } + + public function getDetalleMercancia(): DetalleMercancia + { + return $this->helperGetOrAdd(new DetalleMercancia()); + } + + public function addDetalleMercancia(array $attributes = []): DetalleMercancia + { + $subject = $this->getDetalleMercancia(); + $subject->addAttributes($attributes); + return $subject; + } +} diff --git a/src/CfdiUtils/Elements/CartaPorte20/Mercancias.php b/src/CfdiUtils/Elements/CartaPorte20/Mercancias.php new file mode 100644 index 00000000..af7e1697 --- /dev/null +++ b/src/CfdiUtils/Elements/CartaPorte20/Mercancias.php @@ -0,0 +1,86 @@ +addChild($subject); + return $subject; + } + + public function multiMercancia(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addMercancia($attributes); + } + return $this; + } + + public function getAutotransporte(): Autotransporte + { + return $this->helperGetOrAdd(new Autotransporte()); + } + + public function addAutotransporte(array $attributes = []): Autotransporte + { + $subject = $this->getAutotransporte(); + $subject->addAttributes($attributes); + return $subject; + } + + public function getTransporteMaritimo(): TransporteMaritimo + { + return $this->helperGetOrAdd(new TransporteMaritimo()); + } + + public function addTransporteMaritimo(array $attributes = []): TransporteMaritimo + { + $subject = $this->getTransporteMaritimo(); + $subject->addAttributes($attributes); + return $subject; + } + + public function getTransporteAereo(): TransporteAereo + { + return $this->helperGetOrAdd(new TransporteAereo()); + } + + public function addTransporteAereo(array $attributes = []): TransporteAereo + { + $subject = $this->getTransporteAereo(); + $subject->addAttributes($attributes); + return $subject; + } + + public function getTransporteFerroviario(): TransporteFerroviario + { + return $this->helperGetOrAdd(new TransporteFerroviario()); + } + + public function addTransporteFerroviario(array $attributes = []): TransporteFerroviario + { + $subject = $this->getTransporteFerroviario(); + $subject->addAttributes($attributes); + return $subject; + } +} diff --git a/src/CfdiUtils/Elements/CartaPorte20/PartesTransporte.php b/src/CfdiUtils/Elements/CartaPorte20/PartesTransporte.php new file mode 100644 index 00000000..af96b517 --- /dev/null +++ b/src/CfdiUtils/Elements/CartaPorte20/PartesTransporte.php @@ -0,0 +1,13 @@ +addChild($subject); + return $subject; + } + + public function multiRemolque(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addRemolque($attributes); + } + return $this; + } +} diff --git a/src/CfdiUtils/Elements/CartaPorte20/Seguros.php b/src/CfdiUtils/Elements/CartaPorte20/Seguros.php new file mode 100644 index 00000000..2cacf946 --- /dev/null +++ b/src/CfdiUtils/Elements/CartaPorte20/Seguros.php @@ -0,0 +1,13 @@ +addChild($subject); + return $subject; + } + + public function multiPartesTransporte(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addPartesTransporte($attributes); + } + return $this; + } + + public function getDomicilio(): Domicilio + { + return $this->helperGetOrAdd(new Domicilio()); + } + + public function addDomicilio(array $attributes = []): Domicilio + { + $subject = $this->getDomicilio(); + $subject->addAttributes($attributes); + return $subject; + } +} diff --git a/src/CfdiUtils/Elements/CartaPorte20/TransporteAereo.php b/src/CfdiUtils/Elements/CartaPorte20/TransporteAereo.php new file mode 100644 index 00000000..d6f3b7ab --- /dev/null +++ b/src/CfdiUtils/Elements/CartaPorte20/TransporteAereo.php @@ -0,0 +1,13 @@ +addChild($subject); + return $subject; + } + + public function multiDerechosDePaso(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addDerechosDePaso($attributes); + } + return $this; + } + + public function addCarro(array $attributes = []): Carro + { + $subject = new Carro($attributes); + $this->addChild($subject); + return $subject; + } + + public function multiCarro(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addCarro($attributes); + } + return $this; + } +} diff --git a/src/CfdiUtils/Elements/CartaPorte20/TransporteMaritimo.php b/src/CfdiUtils/Elements/CartaPorte20/TransporteMaritimo.php new file mode 100644 index 00000000..ba26fba6 --- /dev/null +++ b/src/CfdiUtils/Elements/CartaPorte20/TransporteMaritimo.php @@ -0,0 +1,28 @@ +addChild($subject); + return $subject; + } + + public function multiContenedor(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addContenedor($attributes); + } + return $this; + } +} diff --git a/src/CfdiUtils/Elements/CartaPorte20/Ubicacion.php b/src/CfdiUtils/Elements/CartaPorte20/Ubicacion.php new file mode 100644 index 00000000..4f0aba31 --- /dev/null +++ b/src/CfdiUtils/Elements/CartaPorte20/Ubicacion.php @@ -0,0 +1,25 @@ +helperGetOrAdd(new Domicilio()); + } + + public function addDomicilio(array $attributes = []): Domicilio + { + $subject = $this->getDomicilio(); + $subject->addAttributes($attributes); + return $subject; + } +} diff --git a/src/CfdiUtils/Elements/CartaPorte20/Ubicaciones.php b/src/CfdiUtils/Elements/CartaPorte20/Ubicaciones.php new file mode 100644 index 00000000..9da9a1a6 --- /dev/null +++ b/src/CfdiUtils/Elements/CartaPorte20/Ubicaciones.php @@ -0,0 +1,28 @@ +addChild($subject); + return $subject; + } + + public function multiUbicacion(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addUbicacion($attributes); + } + return $this; + } +} diff --git a/tests/CfdiUtilsTests/Elements/CartaPorte20/CartaPorteTest.php b/tests/CfdiUtilsTests/Elements/CartaPorte20/CartaPorteTest.php new file mode 100644 index 00000000..fc93b804 --- /dev/null +++ b/tests/CfdiUtilsTests/Elements/CartaPorte20/CartaPorteTest.php @@ -0,0 +1,238 @@ +assertElementHasName($element, 'cartaporte20:CartaPorte'); + $this->assertElementHasOrder($element, [ + 'cartaporte20:Ubicaciones', + 'cartaporte20:Mercancias', + 'cartaporte20:FiguraTransporte', + ]); + $this->assertElementHasFixedAttributes($element, [ + 'xmlns:cartaporte20' => 'http://www.sat.gob.mx/cartaporte', + 'xsi:schemaLocation' => 'http://www.sat.gob.mx/cartaporte' + . ' http://www.sat.gob.mx/sitio_internet/cfd/CartaPorte/CartaPorte20.xsd', + 'Version' => '2.0', + ]); + $this->assertElementHasChildSingle($element, Ubicaciones::class); + $this->assertElementHasChildSingle($element, Mercancias::class); + $this->assertElementHasChildSingle($element, FiguraTransporte::class); + } + + public function testUbicaciones(): void + { + $element = new Ubicaciones(); + $this->assertElementHasName($element, 'cartaporte20:Ubicaciones'); + $this->assertElementHasChildMultiple($element, Ubicacion::class); + } + + public function testMercancias(): void + { + $element = new Mercancias(); + $this->assertElementHasName($element, 'cartaporte20:Mercancias'); + $this->assertElementHasOrder($element, [ + 'cartaporte20:Mercancia', + 'cartaporte20:Autotransporte', + 'cartaporte20:TransporteMaritimo', + 'cartaporte20:TransporteAereo', + 'cartaporte20:TransporteFerroviario', + ]); + $this->assertElementHasChildMultiple($element, Mercancia::class); + $this->assertElementHasChildSingle($element, Autotransporte::class); + $this->assertElementHasChildSingle($element, TransporteMaritimo::class); + $this->assertElementHasChildSingle($element, TransporteAereo::class); + $this->assertElementHasChildSingle($element, TransporteFerroviario::class); + } + + public function testFiguraTransporte(): void + { + $element = new FiguraTransporte(); + $this->assertElementHasName($element, 'cartaporte20:FiguraTransporte'); + $this->assertElementHasChildMultiple($element, TiposFigura::class); + } + + public function testUbicacion(): void + { + $element = new Ubicacion(); + $this->assertElementHasName($element, 'cartaporte20:Ubicacion'); + $this->assertElementHasChildSingle($element, Domicilio::class); + } + + public function testMercancia(): void + { + $element = new Mercancia(); + $this->assertElementHasName($element, 'cartaporte20:Mercancia'); + $this->assertElementHasOrder($element, [ + 'cartaporte20:Pedimentos', + 'cartaporte20:GuiasIdentificacion', + 'cartaporte20:CantidadTransporta', + 'cartaporte20:DetalleMercancia', + ]); + $this->assertElementHasChildMultiple($element, Pedimentos::class); + $this->assertElementHasChildMultiple($element, GuiasIdentificacion::class); + $this->assertElementHasChildMultiple($element, CantidadTransporta::class); + $this->assertElementHasChildSingle($element, DetalleMercancia::class); + } + + public function testAutotransporte(): void + { + $element = new Autotransporte(); + $this->assertElementHasName($element, 'cartaporte20:Autotransporte'); + $this->assertElementHasOrder($element, [ + 'cartaporte20:IdentificacionVehicular', + 'cartaporte20:Seguros', + 'cartaporte20:Remolques', + ]); + $this->assertElementHasChildSingle($element, IdentificacionVehicular::class); + $this->assertElementHasChildSingle($element, Seguros::class); + $this->assertElementHasChildSingle($element, Remolques::class); + } + + public function testTransporteMaritimo(): void + { + $element = new TransporteMaritimo(); + $this->assertElementHasName($element, 'cartaporte20:TransporteMaritimo'); + $this->assertElementHasChildMultiple($element, Contenedor::class); + } + + public function testTransporteAereo(): void + { + $element = new TransporteAereo(); + $this->assertElementHasName($element, 'cartaporte20:TransporteAereo'); + } + + public function testTransporteFerroviario(): void + { + $element = new TransporteFerroviario(); + $this->assertElementHasName($element, 'cartaporte20:TransporteFerroviario'); + $this->assertElementHasOrder($element, [ + 'cartaporte20:DerechosDePaso', + 'cartaporte20:Carro', + ]); + $this->assertElementHasChildMultiple($element, DerechosDePaso::class); + $this->assertElementHasChildMultiple($element, Carro::class); + } + + public function testDomicilio(): void + { + $element = new Domicilio(); + $this->assertElementHasName($element, 'cartaporte20:Domicilio'); + } + + public function testTiposFigura(): void + { + $element = new TiposFigura(); + $this->assertElementHasName($element, 'cartaporte20:TiposFigura'); + $this->assertElementHasOrder($element, [ + 'cartaporte20:PartesTransporte', + 'cartaporte20:Domicilio', + ]); + $this->assertElementHasChildMultiple($element, PartesTransporte::class); + $this->assertElementHasChildSingle($element, Domicilio::class); + } + + public function testPedimentos(): void + { + $element = new Pedimentos(); + $this->assertElementHasName($element, 'cartaporte20:Pedimentos'); + } + + public function testGuiasIdentificacion(): void + { + $element = new GuiasIdentificacion(); + $this->assertElementHasName($element, 'cartaporte20:GuiasIdentificacion'); + } + + public function testCantidadTransporta(): void + { + $element = new CantidadTransporta(); + $this->assertElementHasName($element, 'cartaporte20:CantidadTransporta'); + } + + public function testDetalleMercancia(): void + { + $element = new DetalleMercancia(); + $this->assertElementHasName($element, 'cartaporte20:DetalleMercancia'); + } + + public function testIdentificacionVehicular(): void + { + $element = new IdentificacionVehicular(); + $this->assertElementHasName($element, 'cartaporte20:IdentificacionVehicular'); + } + + public function testSeguros(): void + { + $element = new Seguros(); + $this->assertElementHasName($element, 'cartaporte20:Seguros'); + } + + public function testRemolques(): void + { + $element = new Remolques(); + $this->assertElementHasName($element, 'cartaporte20:Remolques'); + $this->assertElementHasChildMultiple($element, Remolque::class); + } + + public function testContenedor(): void + { + $element = new Contenedor(); + $this->assertElementHasName($element, 'cartaporte20:Contenedor'); + } + + public function testDerechosDePaso(): void + { + $element = new DerechosDePaso(); + $this->assertElementHasName($element, 'cartaporte20:DerechosDePaso'); + } + + public function testCarro(): void + { + $element = new Carro(); + $this->assertElementHasName($element, 'cartaporte20:Carro'); + $this->assertElementHasChildMultiple($element, Contenedor::class); + } + + public function testPartesTransporte(): void + { + $element = new PartesTransporte(); + $this->assertElementHasName($element, 'cartaporte20:PartesTransporte'); + } + + public function testRemolque(): void + { + $element = new Remolque(); + $this->assertElementHasName($element, 'cartaporte20:Remolque'); + } +} diff --git a/tests/CfdiUtilsTests/Elements/ElementTestCase.php b/tests/CfdiUtilsTests/Elements/ElementTestCase.php new file mode 100644 index 00000000..ab3cc5d8 --- /dev/null +++ b/tests/CfdiUtilsTests/Elements/ElementTestCase.php @@ -0,0 +1,124 @@ +assertSame( + $name, + $element->getElementName(), + sprintf('The element %s must have the name "%s"', get_class($element), $name) + ); + } + + public function assertElementHasFixedAttributes(AbstractElement $element, array $attributes): void + { + $elementClass = get_class($element); + foreach ($attributes as $name => $value) { + $this->assertSame( + $element[$name], + $value, + sprintf('The element %s must have the attribute "%s" with value "%s"', $elementClass, $name, $value) + ); + } + } + + public function assertElementHasOrder(AbstractElement $element, array $order): void + { + $this->assertSame( + $order, + $element->children()->getOrder(), + sprintf('The element %s does not have the correct child order definition', get_class($element)) + ); + } + + public function assertElementHasChildSingle(AbstractElement $element, string $childClassName): void + { + $elementClass = get_class($element); + $childClassBaseName = basename(str_replace('\\', '/', $childClassName)); + $element->children()->removeAll(); + + // element should return the same instance + $getter = 'get' . $childClassBaseName; + $instance = $element->{$getter}(); + $this->assertInstanceOf( + $childClassName, + $instance, + sprintf('The method %s::%s should return the an instance of %s', $elementClass, $getter, $childClassName) + ); + $this->assertSame( + $instance, + $element->{$getter}(), + sprintf('The method %s::%s should return always the same instance', $elementClass, $getter) + ); + + // add should work on the same object + $adder = 'add' . $childClassBaseName; + $second = $element->{$adder}(['foo' => 'bar']); + $this->assertInstanceOf( + $childClassName, + $second, + sprintf('The method %s::%s should return the an instance of %s', $elementClass, $adder, $childClassName) + ); + $this->assertSame( + 'bar', + $instance['foo'], + sprintf('The method %s::%s should write the attributes on the same instance', $elementClass, $adder) + ); + } + + public function assertElementHasChildMultiple(AbstractElement $element, string $childClassName): void + { + $elementClass = get_class($element); + $childClassBaseName = basename(str_replace('\\', '/', $childClassName)); + $element->children()->removeAll(); + + // first: add should return specific instance and added + $adder = 'add' . $childClassBaseName; + $first = $element->{$adder}(['id' => 'first']); + $this->assertInstanceOf( + $childClassName, + $first, + sprintf('The method %s::%s should return the an instance of %s', $elementClass, $adder, $childClassName) + ); + $this->assertSame( + 'first', + $first['id'], + sprintf('The method %s::%s should write the attributes', $elementClass, $adder) + ); + $this->assertCount(1, $element->children()); + + // second: add should return other instance different from first but added + $second = $element->{$adder}(['id' => 'second']); + $this->assertNotSame( + $first, + $second, + sprintf('The method %s::%s should return a new instance of %s', $elementClass, $adder, $childClassName) + ); + $this->assertSame( + 'second', + $second['id'], + sprintf('The method %s::%s should write the attributes', $elementClass, $adder) + ); + $this->assertCount(2, $element->children()); + + // multiple: add should return other instance different from first but added + $multier = 'multi' . $childClassBaseName; + $sameAsElement = $element->{$multier}(['id' => 'third'], ['id' => 'fourth']); + $this->assertSame( + $element, + $sameAsElement, + sprintf( + 'The method %s::%s should return the same element as the instance contained', + $elementClass, + $multier + ) + ); + $this->assertCount(4, $element->children()); + } +} From 0a32c34a5421679ac2d0901f7e25fd9a4c369bf8 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 17 Dec 2021 12:04:42 -0600 Subject: [PATCH 3/4] Fix dev:coverage script --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4ad091a7..ee19cc19 100644 --- a/composer.json +++ b/composer.json @@ -83,7 +83,7 @@ "@php vendor/bin/phpstan analyse --no-progress src/ tests/" ], "dev:coverage": [ - "@php -dzend_extension=xdebug.so vendor/bin/phpunit --coverage-text --coverage-html build/coverage/html/" + "@php -dzend_extension=xdebug.so -dxdebug.mode=coverage vendor/bin/phpunit --coverage-html build/coverage/html/" ] }, "scripts-descriptions": { From b42568a282e2300ac2c25f399d5f1fc081df1631 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 17 Dec 2021 12:12:32 -0600 Subject: [PATCH 4/4] Prepare version 2.18.0 --- docs/CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fd4cc708..d43d6c63 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -33,6 +33,15 @@ The following changes apply only to development and has been applied to main branch. +## Version 2.18.0 2021-12-17 + +Add `CfdiUtils\Elements\CartaPorte20` *Elements* to work with "Carta Porte 2.0". + +Add *Elements Maker*, a development tool to create element classes based on a specification file. + +Fix `dev:coverage` composer script. + + ## Version 2.17.0 2021-12-10 The helper object `SumasConceptosWriter` also writes the sum of *impuestos locales* when they are present.