diff --git a/.gitattributes b/.gitattributes
index 5b5ddb3..1f0f9eb 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,6 +2,7 @@
* text=auto
# Do not put this files on a distribution package (by .gitignore)
+/tools/ export-ignore
/vendor/ export-ignore
/composer.lock export-ignore
.phpunit.result.cache export-ignore
@@ -9,6 +10,7 @@
# Do not put this files on a distribution package
/build/ export-ignore
/tests/ export-ignore
+/.phive/ export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.php_cs.dist export-ignore
diff --git a/.gitignore b/.gitignore
index 840d101..d69384a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
# do not include this files on git
-/vendor
+/tools/
+/vendor/
/composer.lock
.phpunit.result.cache
diff --git a/.phive/phars.xml b/.phive/phars.xml
new file mode 100644
index 0000000..a4a614e
--- /dev/null
+++ b/.phive/phars.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/.php_cs.dist b/.php_cs.dist
index 5ae8296..ef6a952 100644
--- a/.php_cs.dist
+++ b/.php_cs.dist
@@ -46,6 +46,6 @@ return PhpCsFixer\Config::create()
->setFinder(
PhpCsFixer\Finder::create()
->in(__DIR__)
- ->exclude(['vendor', 'build'])
+ ->exclude(['tools', 'vendor', 'build'])
)
;
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
index 8a08383..6ee0f9b 100644
--- a/.scrutinizer.yml
+++ b/.scrutinizer.yml
@@ -7,16 +7,15 @@ filter:
build:
dependencies:
override:
- - composer self-update --no-interaction --no-progress
- - composer remove squizlabs/php_codesniffer friendsofphp/php-cs-fixer phpstan/phpstan --dev --no-interaction --no-progress --no-update
- - composer install --no-interaction --prefer-dist
+ - composer update --no-interaction --prefer-dist
nodes:
- analysis: # see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/
- project_setup: {override: true}
+ analysis: # see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/
+ project_setup:
+ override: true
tests:
override:
- php-scrutinizer-run --enable-security-analysis
- - command: vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover
+ - command: vendor/bin/phpunit --verbose --testdox --coverage-clover=coverage.clover
coverage:
file: coverage.clover
format: clover
diff --git a/.travis.yml b/.travis.yml
index 7ad80ed..17ed2a2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,21 +6,21 @@ php: ["7.3", "7.4", "8.0"]
cache:
- directories:
- $HOME/.composer
-
-env:
- global:
- - PHP_CS_FIXER_IGNORE_ENV=yes
+ - $HOME/.phive
before_script:
- phpenv config-rm xdebug.ini || true
- travis_retry composer self-update --no-interaction --2
- travis_retry composer upgrade --no-interaction --prefer-dist
+ - wget -O phive https://phar.io/releases/phive.phar && chmod +x phive
+ - ./phive install --force-accept-unsigned --trust-gpg-keys 0x4AA394086372C20A,0x31C7E470E2138192,0xE82B2FB314E9906E,0xCF1A108D0E7AE720,0xC5095986493B4AA0
script:
- - vendor/bin/php-cs-fixer fix --dry-run --verbose
- - vendor/bin/phpcbf --colors -sp src/ tests/ bin/
+ - PHP_CS_FIXER_IGNORE_ENV=yes tools/php-cs-fixer fix --dry-run --verbose
+ - tools/phpcs --colors -sp src/ tests/ bin/
- vendor/bin/phpunit --testdox --verbose
- - vendor/bin/phpstan analyse --no-progress --verbose --level max src/ tests/ bin/
+ - tools/phpstan analyse --no-progress --verbose --level max src/ tests/ bin/
+ - bash bin/check-current-max-occurs-paths.bash
notifications:
email:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5dc6b4b..5b67cd8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -62,8 +62,15 @@ Considera las siguientes directrices:
## Proceso de construcción
```shell
+# Instala phive, sigue las indicaciones seguras de https://phar.io/#Install o la forma insegura:
+wget https://phar.io/releases/phive.phar -O ~/.local/bin/phive
+
+## Si no se han instalado las herramientas previamente
+phive install --force-accept-unsigned --trust-gpg-keys 0x4AA394086372C20A,0x31C7E470E2138192,0xE82B2FB314E9906E,0xCF1A108D0E7AE720,0xC5095986493B4AA0
+
# Actualiza tus dependencias
composer update
+phive update
# Verificación de estilo de código
composer dev:check-style
@@ -76,6 +83,9 @@ composer dev:test
# Ejecución todo en uno, corregir estilo, verificar estilo y correr pruebas
composer dev:build
+
+# Opcional: correr las pruebas de mutación
+composer dev:infection
```
[phpCfdi]: https://github.com/phpcfdi/
diff --git a/bin/check-current-max-occurs-paths.bash b/bin/check-current-max-occurs-paths.bash
new file mode 100644
index 0000000..f4b1849
--- /dev/null
+++ b/bin/check-current-max-occurs-paths.bash
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash -e
+
+TEMPFILE="$(mktemp)"
+BINPATH="$(dirname $0)"
+
+echo "Creating UnboundedOccursPaths.json on $TEMPFILE"
+php "${BINPATH}/max-occurs-paths.php" > "$TEMPFILE"
+
+echo "Comparing to current UnboundedOccursPaths.json"
+diff -u -b -B "${BINPATH}/../src/UnboundedOccursPaths.json" "$TEMPFILE"
+
+echo "OK: Files match"
+rm "$TEMPFILE"
diff --git a/composer.json b/composer.json
index 77839e6..c6a48b3 100644
--- a/composer.json
+++ b/composer.json
@@ -17,10 +17,7 @@
"ext-json": "*"
},
"require-dev": {
- "phpunit/phpunit": "^9.3",
- "squizlabs/php_codesniffer": "^3.0",
- "friendsofphp/php-cs-fixer": "^2.4",
- "phpstan/phpstan": "^0.12"
+ "phpunit/phpunit": "^9.5"
},
"autoload": {
"psr-4": {
@@ -35,26 +32,30 @@
"scripts": {
"dev:build": ["@dev:fix-style", "@dev:check-style", "@dev:test"],
"dev:check-style": [
- "vendor/bin/php-cs-fixer fix --dry-run --verbose",
- "vendor/bin/phpcs --colors -sp src/ tests/ bin/"
+ "@php tools/php-cs-fixer fix --dry-run --verbose",
+ "@php tools/phpcs --colors -sp src/ tests/ bin/"
],
"dev:fix-style": [
- "vendor/bin/php-cs-fixer fix --verbose",
- "vendor/bin/phpcbf --colors -sp src/ tests/ bin/"
+ "@php tools/php-cs-fixer fix --verbose",
+ "@php tools/phpcbf --colors -sp src/ tests/ bin/"
],
"dev:test": [
- "vendor/bin/phpunit --testdox --verbose --stop-on-failure",
- "vendor/bin/phpstan analyse --verbose --level max src/ tests/ bin/"
+ "@php vendor/bin/phpunit --testdox --verbose --stop-on-failure",
+ "@php tools/phpstan analyse --verbose --level max src/ tests/ bin/"
],
"dev:coverage": [
- "@php -dzend_extension=xdebug.so vendor/bin/phpunit --coverage-html build/coverage/html/"
+ "@php -dzend_extension=xdebug.so -dxdebug.mode=coverage vendor/bin/phpunit --coverage-xml build/coverage/xml/ --coverage-html build/coverage/html/"
+ ],
+ "dev:infection": [
+ "@php tools/infection --show-mutations --no-progress"
]
},
"scripts-descriptions": {
"dev:build": "DEV: run dev:fix-style and dev:tests, run before pull request",
"dev:check-style": "DEV: search for code style errors using php-cs-fixer and phpcs",
"dev:fix-style": "DEV: fix code style errors using php-cs-fixer and phpcbf",
- "dev:test": "DEV: run phpunit and phpstan",
- "dev:coverage": "DEV: run phpunit with xdebug and storage coverage in build/coverage/html/"
+ "dev:test": "DEV: run dev:fix-style, phpunit and phpstan",
+ "dev:coverage": "DEV: run phpunit with xdebug and storage coverage in build/coverage/html/",
+ "dev:infection": "DEV: run mutation tests using infection"
}
}
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index f990e2f..ed6b4b6 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -11,6 +11,15 @@ versión aunque sí su incorporación en la rama principal de trabajo, generalme
## Listado de cambios
+### Versión 0.2.0 2021-03-22
+
+- Se extrae la lógica del conteo de hijos de `Nodes\Children` a `Nodes\KeysCounter`.
+- Se corrigen los test y las llamadas de `file_get_contents`.
+- Conseguir el 100% de testeo.
+- Agregar a Travis-CI la comprobación de que el archivo `src/UnboundedOccursPaths.json` no ha cambiado.
+- Usar `phive` para las herramientas de desarrollo.
+- Se agrega `infection` para correr pruebas de mutación. No es mandatorio por el momento.
+
### Versión 0.1.0 2021-02-02 ¡Feliz cumpleaños Dany!
- Primera liberación para su uso público.
diff --git a/docs/TODO.md b/docs/TODO.md
index 1dea29d..2b86145 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -2,8 +2,6 @@
## Ideas, por favor, levante un ticket para discutirlas
-- Agregar a Travis la comprobación del archivo `src/UnboundedOccursPaths.json`
-
- ¿Los elementos `Comprobante/Complemento` deberían colapsarse?
-- Usar infection
+- Hacer que `infection` se ejecute con un mínimo requerido en la integración contínua.
diff --git a/infection.json.dist b/infection.json.dist
new file mode 100644
index 0000000..3c0deae
--- /dev/null
+++ b/infection.json.dist
@@ -0,0 +1,14 @@
+{
+ "source": {
+ "directories": [
+ "src"
+ ]
+ },
+ "logs": {
+ "text": "build\/infection.log"
+ },
+ "mutators": {
+ "@default": true
+ },
+ "initialTestsPhpOptions": "-dzend_extension=xdebug.so -dxdebug.mode=coverage"
+}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 24596ad..dabf248 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -5,6 +5,7 @@
+
diff --git a/src/CfdiToDataNode.php b/src/CfdiToDataNode.php
index cb16a4b..0f82c65 100644
--- a/src/CfdiToDataNode.php
+++ b/src/CfdiToDataNode.php
@@ -15,9 +15,14 @@ final class CfdiToDataNode
/** @var UnboundedOccursPaths */
private $unboundedOccursPaths;
- public function __construct(UnboundedOccursPaths $multipleChildrenPaths)
+ public function __construct(UnboundedOccursPaths $unboundedOccursPaths)
{
- $this->unboundedOccursPaths = $multipleChildrenPaths;
+ $this->unboundedOccursPaths = $unboundedOccursPaths;
+ }
+
+ public function getUnboundedOccursPaths(): UnboundedOccursPaths
+ {
+ return $this->unboundedOccursPaths;
}
public function convertXmlContent(string $xmlContents): Nodes\Node
diff --git a/src/Nodes/Children.php b/src/Nodes/Children.php
index 26df61f..890af83 100644
--- a/src/Nodes/Children.php
+++ b/src/Nodes/Children.php
@@ -14,31 +14,27 @@ final class Children
/** @var UnboundedOccursPaths */
private $unboundedOccursPaths;
- /** @var array */
- private $childrenCountByKey = [];
+ /** @var KeysCounter */
+ private $keysCounter;
public function __construct(UnboundedOccursPaths $unboundedOccursPaths)
{
$this->unboundedOccursPaths = $unboundedOccursPaths;
+ $this->keysCounter = new KeysCounter();
}
public function append(Node $child): void
{
$this->children[] = $child;
- $this->childrenCountByKey[$child->getKey()] = $this->getChildrenCountByKey($child->getKey());
+ $this->keysCounter->register($child->getKey());
}
public function isChildrenMultiple(Node $child): bool
{
- return ($this->getChildrenCountByKey($child->getKey()) > 1)
+ return $this->keysCounter->hasMany($child->getKey())
|| $this->unboundedOccursPaths->match($child->getPath());
}
- private function getChildrenCountByKey(string $key): int
- {
- return $this->childrenCountByKey[$key] ?? 0;
- }
-
/** @return array */
public function toArray(): array
{
diff --git a/src/Nodes/KeysCounter.php b/src/Nodes/KeysCounter.php
new file mode 100644
index 0000000..cbf721e
--- /dev/null
+++ b/src/Nodes/KeysCounter.php
@@ -0,0 +1,26 @@
+ */
+ private $counts;
+
+ public function register(string $key): void
+ {
+ $this->counts[$key] = $this->get($key) + 1;
+ }
+
+ public function get(string $key): int
+ {
+ return $this->counts[$key] ?? 0;
+ }
+
+ public function hasMany(string $key): bool
+ {
+ return $this->get($key) > 1;
+ }
+}
diff --git a/src/XsdMaxOccurs/Downloader.php b/src/XsdMaxOccurs/Downloader.php
index 84a1a42..b9d67e5 100644
--- a/src/XsdMaxOccurs/Downloader.php
+++ b/src/XsdMaxOccurs/Downloader.php
@@ -5,17 +5,13 @@
namespace PhpCfdi\CfdiToJson\XsdMaxOccurs;
use RuntimeException;
-use Throwable;
final class Downloader implements DownloaderInterface
{
public function get(string $url): string
{
- try {
- $contents = file_get_contents($url);
- } catch (Throwable $exception) {
- throw new RuntimeException("Unable to get $url contents", 0, $exception);
- }
+ /** @noinspection PhpUsageOfSilenceOperatorInspection */
+ $contents = @file_get_contents($url);
if (false === $contents) {
throw new RuntimeException("Unable to get $url contents");
}
diff --git a/src/XsdMaxOccurs/Finder.php b/src/XsdMaxOccurs/Finder.php
index 8d978d3..a614160 100644
--- a/src/XsdMaxOccurs/Finder.php
+++ b/src/XsdMaxOccurs/Finder.php
@@ -17,9 +17,9 @@ public function obtainPathsFromXsdContents(string $xsdContents): array
$document->loadXML($xsdContents);
return array_merge(
- $this->obtainPathsForXPathQuery($document, '//xs:element[@maxOccurs="unbounded"]'),
- $this->obtainPathsForXPathQuery($document, '//xs:sequence[@maxOccurs="unbounded"]/xs:element'),
- $this->obtainPathsForXPathQuery($document, '//xs:choice[@maxOccurs="unbounded"]/xs:element'),
+ $this->obtainPathsForXPathQuery($document, '//x:element[@maxOccurs="unbounded"]'),
+ $this->obtainPathsForXPathQuery($document, '//x:sequence[@maxOccurs="unbounded"]/x:element'),
+ $this->obtainPathsForXPathQuery($document, '//x:choice[@maxOccurs="unbounded"]/x:element'),
);
}
@@ -32,7 +32,7 @@ private function obtainPathsForXPathQuery(DOMDocument $document, string $query):
{
$paths = [];
$xpath = new DOMXPath($document);
- $xpath->registerNamespace('xs', 'http://www.w3.org/2001/XMLSchema');
+ $xpath->registerNamespace('x', 'http://www.w3.org/2001/XMLSchema');
$nodes = $xpath->query($query) ?: new DOMNodeList();
foreach ($nodes as $node) {
if ($node instanceof DOMElement) {
diff --git a/tests/Functional/ConverterTest.php b/tests/Functional/ConverterTest.php
index 4b9ec21..44fe33c 100644
--- a/tests/Functional/ConverterTest.php
+++ b/tests/Functional/ConverterTest.php
@@ -57,8 +57,10 @@ public function testConverterExportsNodesAsArrayWhenTheyAreKnownFromComplemento(
public function testJsonConverter(): void
{
$xmlContents = $this->fileContents('cfdi-example.xml');
+ $jsonFile = $this->filePath('cfdi-example.json');
/** @noinspection PhpUnhandledExceptionInspection */
$json = JsonConverter::convertToJson($xmlContents);
- $this->assertJsonStringEqualsJsonString(json_encode($this->data) ?: '', $json);
+ $this->assertJsonStringEqualsJsonFile($jsonFile, $json);
+ $this->assertStringEqualsFile($jsonFile, $json, 'Check that the default format is preserved');
}
}
diff --git a/tests/Unit/FactoryTest.php b/tests/Unit/FactoryTest.php
new file mode 100644
index 0000000..ac62c50
--- /dev/null
+++ b/tests/Unit/FactoryTest.php
@@ -0,0 +1,80 @@
+assertEquals($factory->createDefaultUnboundedOccursPaths(), $factory->getUnboundedOccursPaths());
+ }
+
+ public function testConstructFactoryUsesGivenUnboundedOccursPaths(): void
+ {
+ $unboundedOccursPaths = new UnboundedOccursPaths();
+ $factory = new Factory($unboundedOccursPaths);
+ $this->assertEquals($unboundedOccursPaths, $factory->getUnboundedOccursPaths());
+ $converter = $factory->createConverter();
+ $this->assertSame($unboundedOccursPaths, $converter->getUnboundedOccursPaths());
+ }
+
+ public function testCreateUnboundedOccursPathsUsingJsonFileUsingInvalidFileWithErrorReporting(): void
+ {
+ $factory = new Factory(new UnboundedOccursPaths());
+ $this->expectWarning();
+ $factory->createUnboundedOccursPathsUsingJsonFile(__DIR__ . '/not-found');
+ }
+
+ public function testCreateUnboundedOccursPathsUsingJsonFileUsingInvalidFileWithoutErrorReporting(): void
+ {
+ $factory = new Factory(new UnboundedOccursPaths());
+
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Unable to open file');
+
+ /** @noinspection PhpUsageOfSilenceOperatorInspection */
+ @$factory->createUnboundedOccursPathsUsingJsonFile(__DIR__ . '/not-found');
+ }
+
+ public function testCreateUnboundedOccursPathsUsingJsonFileUsingInvalidContents(): void
+ {
+ $factory = new Factory(new UnboundedOccursPaths());
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('has invalid contents');
+ $factory->createUnboundedOccursPathsUsingJsonFile(__FILE__);
+ }
+
+ public function testCreateUnboundedOccursPathsUsingJsonSourceWithInvalidJson(): void
+ {
+ $factory = new Factory(new UnboundedOccursPaths());
+ $this->expectException(JsonException::class);
+ $factory->createUnboundedOccursPathsUsingJsonSource('');
+ }
+
+ public function testCreateUnboundedOccursPathsUsingJsonNotArray(): void
+ {
+ $factory = new Factory(new UnboundedOccursPaths());
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('JSON does not contains an array of entries');
+ $factory->createUnboundedOccursPathsUsingJsonSource('""');
+ }
+
+ public function testCreateUnboundedOccursPathsUsingJsonNotArrayOfStrings(): void
+ {
+ $factory = new Factory(new UnboundedOccursPaths());
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('JSON does not contains a string on index 1');
+ $factory->createUnboundedOccursPathsUsingJsonSource('["string", 2]');
+ }
+}
diff --git a/tests/Unit/Nodes/ChildrenTest.php b/tests/Unit/Nodes/ChildrenTest.php
new file mode 100644
index 0000000..e8b06a7
--- /dev/null
+++ b/tests/Unit/Nodes/ChildrenTest.php
@@ -0,0 +1,24 @@
+append($nodeChapter);
+ $children->append($nodeChapter);
+
+ $this->assertTrue($children->isChildrenMultiple($nodeChapter));
+ }
+}
diff --git a/tests/Unit/Nodes/KeysCounterTest.php b/tests/Unit/Nodes/KeysCounterTest.php
new file mode 100644
index 0000000..e1c6184
--- /dev/null
+++ b/tests/Unit/Nodes/KeysCounterTest.php
@@ -0,0 +1,38 @@
+register($key);
+ }
+ return $counter;
+ }
+
+ public function testHasMany(): void
+ {
+ $counter = $this->createPopulatedCounter('author', 'chapter', 'chapter');
+
+ $this->assertTrue($counter->hasMany('chapter'));
+ $this->assertFalse($counter->hasMany('author'));
+ $this->assertFalse($counter->hasMany('non-existent'));
+ }
+
+ public function testGet(): void
+ {
+ $counter = $this->createPopulatedCounter('author', 'chapter', 'chapter');
+
+ $this->assertSame(2, $counter->get('chapter'));
+ $this->assertSame(1, $counter->get('author'));
+ $this->assertSame(0, $counter->get('non-existent'));
+ }
+}
diff --git a/tests/Unit/XsdMaxOccurs/DownloaderTest.php b/tests/Unit/XsdMaxOccurs/DownloaderTest.php
index d970b1a..3031ba1 100644
--- a/tests/Unit/XsdMaxOccurs/DownloaderTest.php
+++ b/tests/Unit/XsdMaxOccurs/DownloaderTest.php
@@ -22,16 +22,14 @@ public function testDownloaderThrowsExceptionWhenCannotGetContents(): void
public function testDownloaderThrowsExceptionWhenCannotGetContentsWithoutErrorReporting(): void
{
- $previousErrorReporting = error_reporting(0);
-
$url = __DIR__ . '/file-not-found';
$downloader = new Downloader();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage("Unable to get $url contents");
- $downloader->get($url);
- error_reporting($previousErrorReporting);
+ /** @noinspection PhpUsageOfSilenceOperatorInspection */
+ @$downloader->get($url);
}
public function testDownloaderThrowsExceptionWhenContentIsEmpty(): void
diff --git a/tests/_files/cfdi-example.json b/tests/_files/cfdi-example.json
new file mode 100644
index 0000000..34fbaa1
--- /dev/null
+++ b/tests/_files/cfdi-example.json
@@ -0,0 +1,115 @@
+{
+ "Certificado": "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",
+ "CondicionesDePago": "CONTADO",
+ "Fecha": "2018-01-12T08:15:01",
+ "Folio": "11541",
+ "FormaPago": "04",
+ "LugarExpedicion": "76802",
+ "MetodoPago": "PUE",
+ "Moneda": "MXN",
+ "NoCertificado": "00001000000401220451",
+ "Sello": "Xt7tK83WumikNMyx4Y/Z3R7D0rOjqTrrLu8wBlCnvXrpMFgWtyrcFUttGnevvUqCnQjuVUSpFcXqbzIQEUYNKFjxmtjwGHN+b15xUvcnfqpJRBoJe2IKd5YMZqYp9NhTJIMBYsE7+fhP1+mHcKdKn9WwXrar8uXzISqPgZ97AORBsMWmXxbVWYRtqT4MX/Xq4yhbT4jaoaut5AwhVzE1TUyZ10/C2gGySQeFVyEp9aqNScIxPotVDb7fMIWxsV26XODf6GK14B0TJNmRlCIfmfb2rQeskiYeiF5AQPb6Z2gmGLHcNks7qC+eO3EsGVr1/ntmGcwTurbGXmE4/OAgdg==",
+ "Serie": "H",
+ "SubTotal": "1709.12",
+ "TipoDeComprobante": "I",
+ "Total": "2010.01",
+ "Version": "3.3",
+ "xsi:schemaLocation": "http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd http://www.sat.gob.mx/implocal http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.xsd",
+ "Emisor": {
+ "Nombre": "PROMOTORA OTIR SA DE CV",
+ "RegimenFiscal": "601",
+ "Rfc": "POT9207213D6"
+ },
+ "Receptor": {
+ "Nombre": "DAY INTERNATIONAL DE MEXICO SA DE CV",
+ "Rfc": "DIM8701081LA",
+ "UsoCFDI": "G03"
+ },
+ "Conceptos": {
+ "Concepto": [
+ {
+ "Cantidad": "2.00",
+ "ClaveProdServ": "90111501",
+ "ClaveUnidad": "E48",
+ "Descripcion": "Paquete",
+ "Importe": "1355.67",
+ "Unidad": "UNIDAD DE SERVICIO",
+ "ValorUnitario": "677.83",
+ "Impuestos": {
+ "Traslados": {
+ "Traslado": [
+ {
+ "Base": "1355.67",
+ "Importe": "216.91",
+ "Impuesto": "002",
+ "TasaOCuota": "0.160000",
+ "TipoFactor": "Tasa"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "Cantidad": "1.00",
+ "ClaveProdServ": "90101501",
+ "ClaveUnidad": "E48",
+ "Descripcion": "Restaurante",
+ "Importe": "353.45",
+ "Unidad": "UNIDAD DE SERVICIO",
+ "ValorUnitario": "353.45",
+ "Impuestos": {
+ "Traslados": {
+ "Traslado": [
+ {
+ "Base": "353.45",
+ "Importe": "56.55",
+ "Impuesto": "002",
+ "TasaOCuota": "0.160000",
+ "TipoFactor": "Tasa"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ },
+ "Impuestos": {
+ "TotalImpuestosTrasladados": "273.46",
+ "Traslados": {
+ "Traslado": [
+ {
+ "Importe": "273.46",
+ "Impuesto": "002",
+ "TasaOCuota": "0.160000",
+ "TipoFactor": "Tasa"
+ }
+ ]
+ }
+ },
+ "Complemento": [
+ {
+ "ImpuestosLocales": {
+ "TotaldeRetenciones": "0.00",
+ "TotaldeTraslados": "27.43",
+ "version": "1.0",
+ "TrasladosLocales": [
+ {
+ "ImpLocTrasladado": "IH",
+ "Importe": "27.43",
+ "TasadeTraslado": "2.50"
+ }
+ ]
+ },
+ "TimbreFiscalDigital": {
+ "FechaTimbrado": "2018-01-12T08:17:54",
+ "NoCertificadoSAT": "00001000000406258094",
+ "RfcProvCertif": "DCD090706E42",
+ "SelloCFD": "Xt7tK83WumikNMyx4Y/Z3R7D0rOjqTrrLu8wBlCnvXrpMFgWtyrcFUttGnevvUqCnQjuVUSpFcXqbzIQEUYNKFjxmtjwGHN+b15xUvcnfqpJRBoJe2IKd5YMZqYp9NhTJIMBYsE7+fhP1+mHcKdKn9WwXrar8uXzISqPgZ97AORBsMWmXxbVWYRtqT4MX/Xq4yhbT4jaoaut5AwhVzE1TUyZ10/C2gGySQeFVyEp9aqNScIxPotVDb7fMIWxsV26XODf6GK14B0TJNmRlCIfmfb2rQeskiYeiF5AQPb6Z2gmGLHcNks7qC+eO3EsGVr1/ntmGcwTurbGXmE4/OAgdg==",
+ "SelloSAT": "IRy7wQnKnlIsN/pSZSR7qEm/SOJuLIbNjj/S3EAd278T2uo0t73KXfXzUbbfWOwpdZEAZeosq/yEiStTaf44ZonqRS1fq6oYk12udMmT4NFrEYbPEEKLn4lqdhuW4v8ZK2Vos/pjCtYtpT+/oVIXiWg9KrGVGuMvygRPWSmd+YJq3Jm7qTz0ON0vzBOvXralSZ4Q14xUvt6ZDM9gYqIzTtCjIWaNrAdEYyqfZjvfy0uCyThh6HvCbMsX9gq4RsQj3SIoA56g+1SJevoZ6Jr722mDCLcPox3KCN75Bk8ALJI6G0weP7rQO5jEtulTRNWN3w+tlryZWElkD79MDZA6Zg==",
+ "UUID": "CEE4BE01-ADFA-4DEB-8421-ADD60F0BEDAC",
+ "Version": "1.1",
+ "xsi:schemaLocation": "http://www.sat.gob.mx/TimbreFiscalDigital http://www.sat.gob.mx/sitio_internet/cfd/TimbreFiscalDigital/TimbreFiscalDigitalv11.xsd"
+ }
+ }
+ ]
+}
\ No newline at end of file