diff --git a/Cargo.lock b/Cargo.lock
index e2a86fad..d1792fe9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,32 @@
# It is not intended for manual editing.
version = 3
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aes"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
[[package]]
name = "ahash"
version = "0.7.7"
@@ -49,12 +75,45 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+[[package]]
+name = "backtrace"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
+
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+[[package]]
+name = "bitflags"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
+
[[package]]
name = "bitvec"
version = "1.0.1"
@@ -150,12 +209,34 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
+[[package]]
+name = "bzip2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
+dependencies = [
+ "bzip2-sys",
+ "libc",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.11+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
+ "jobserver",
"libc",
]
@@ -183,7 +264,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
- "windows-targets",
+ "windows-targets 0.48.5",
]
[[package]]
@@ -208,6 +289,32 @@ dependencies = [
"phf_codegen",
]
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "constant_time_eq"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
@@ -275,6 +382,15 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "deranged"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc"
+dependencies = [
+ "powerfmt",
+]
+
[[package]]
name = "deunicode"
version = "1.4.1"
@@ -289,6 +405,16 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
+dependencies = [
+ "cfg-if",
]
[[package]]
@@ -297,12 +423,107 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+[[package]]
+name = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+
+[[package]]
+name = "flate2"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+[[package]]
+name = "futures-channel"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
+
+[[package]]
+name = "futures-task"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
+
+[[package]]
+name = "futures-util"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+]
+
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -324,6 +545,12 @@ dependencies = [
"wasi",
]
+[[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+
[[package]]
name = "globset"
version = "0.4.14"
@@ -343,11 +570,30 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
"ignore",
"walkdir",
]
+[[package]]
+name = "h2"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -363,6 +609,49 @@ version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "http"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
[[package]]
name = "humansize"
version = "2.1.3"
@@ -372,6 +661,43 @@ dependencies = [
"libm",
]
+[[package]]
+name = "hyper"
+version = "0.14.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2 0.4.10",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
[[package]]
name = "iana-time-zone"
version = "0.1.58"
@@ -395,6 +721,16 @@ dependencies = [
"cc",
]
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
[[package]]
name = "ignore"
version = "0.4.21"
@@ -421,12 +757,36 @@ dependencies = [
"hashbrown 0.14.3",
]
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+[[package]]
+name = "jobserver"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "js-sys"
version = "0.3.66"
@@ -454,6 +814,12 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
+
[[package]]
name = "log"
version = "0.4.20"
@@ -475,6 +841,50 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
[[package]]
name = "num-traits"
version = "0.2.17"
@@ -484,12 +894,65 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "object"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+[[package]]
+name = "openssl"
+version = "0.10.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45"
+dependencies = [
+ "bitflags 2.4.1",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.39",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "parse-zoneinfo"
version = "0.3.0"
@@ -499,6 +962,29 @@ dependencies = [
"regex",
]
+[[package]]
+name = "password-hash"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
+dependencies = [
+ "base64ct",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "pbkdf2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
+dependencies = [
+ "digest",
+ "hmac",
+ "password-hash",
+ "sha2",
+]
+
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -588,6 +1074,30 @@ dependencies = [
"siphasher",
]
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@@ -701,6 +1211,15 @@ dependencies = [
"getrandom",
]
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
[[package]]
name = "regex"
version = "1.10.2"
@@ -736,7 +1255,45 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd"
dependencies = [
- "bytecheck",
+ "bytecheck",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.11.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
]
[[package]]
@@ -793,6 +1350,25 @@ dependencies = [
"rust_decimal",
]
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustix"
+version = "0.38.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
+dependencies = [
+ "bitflags 2.4.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "ryu"
version = "1.0.16"
@@ -808,6 +1384,15 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "schannel"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -820,6 +1405,29 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+[[package]]
+name = "security-framework"
+version = "2.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "serde"
version = "1.0.193"
@@ -851,6 +1459,29 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
[[package]]
name = "sha2"
version = "0.10.8"
@@ -874,6 +1505,15 @@ version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "slug"
version = "0.1.5"
@@ -884,6 +1524,32 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "socket2"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
+dependencies = [
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "subtle"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+
[[package]]
name = "syn"
version = "1.0.109"
@@ -918,12 +1584,46 @@ dependencies = [
"syn 2.0.39",
]
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+[[package]]
+name = "tempfile"
+version = "3.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "redox_syscall",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "tera"
version = "1.19.1"
@@ -966,6 +1666,24 @@ dependencies = [
"syn 2.0.39",
]
+[[package]]
+name = "time"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e"
+dependencies = [
+ "deranged",
+ "powerfmt",
+ "serde",
+ "time-core",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
[[package]]
name = "tinyvec"
version = "1.6.0"
@@ -981,6 +1699,57 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+[[package]]
+name = "tokio"
+version = "1.35.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2 0.5.5",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.39",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
[[package]]
name = "toml_datetime"
version = "0.6.3"
@@ -998,6 +1767,37 @@ dependencies = [
"winnow",
]
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
[[package]]
name = "typenum"
version = "1.17.0"
@@ -1060,18 +1860,50 @@ dependencies = [
"unic-common",
]
+[[package]]
+name = "unicode-bidi"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416"
+
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
[[package]]
name = "uuid"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
[[package]]
name = "version_check"
version = "0.9.4"
@@ -1088,6 +1920,15 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@@ -1119,6 +1960,18 @@ dependencies = [
"wasm-bindgen-shared",
]
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.89"
@@ -1148,6 +2001,16 @@ version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
+[[package]]
+name = "web-sys"
+version = "0.3.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "winapi"
version = "0.3.9"
@@ -1185,7 +2048,25 @@ version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
dependencies = [
- "windows-targets",
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.0",
]
[[package]]
@@ -1194,13 +2075,28 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.0",
+ "windows_aarch64_msvc 0.52.0",
+ "windows_i686_gnu 0.52.0",
+ "windows_i686_msvc 0.52.0",
+ "windows_x86_64_gnu 0.52.0",
+ "windows_x86_64_gnullvm 0.52.0",
+ "windows_x86_64_msvc 0.52.0",
]
[[package]]
@@ -1209,42 +2105,84 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+
[[package]]
name = "winnow"
version = "0.5.26"
@@ -1254,6 +2192,16 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "wyz"
version = "0.5.1"
@@ -1297,10 +2245,14 @@ checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a"
name = "xsender"
version = "0.1.0"
dependencies = [
+ "base64",
"lazy_static",
"log",
+ "regex",
+ "reqwest",
"serde",
"tera",
+ "tokio",
"xml",
"zip",
]
@@ -1311,7 +2263,45 @@ version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
+ "aes",
"byteorder",
+ "bzip2",
+ "constant_time_eq",
"crc32fast",
"crossbeam-utils",
+ "flate2",
+ "hmac",
+ "pbkdf2",
+ "sha1",
+ "time",
+ "zstd",
+]
+
+[[package]]
+name = "zstd"
+version = "0.11.2+zstd.1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "5.0.2+zstd.1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
+dependencies = [
+ "libc",
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.9+zstd.1.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656"
+dependencies = [
+ "cc",
+ "pkg-config",
]
diff --git a/xsender/Cargo.toml b/xsender/Cargo.toml
index 7f5af55f..3911708e 100644
--- a/xsender/Cargo.toml
+++ b/xsender/Cargo.toml
@@ -8,7 +8,13 @@ description = "Sends XML files through SOAP - SUNAT"
[dependencies]
xml = "0.8.10"
log = "0.4.20"
-zip = { version = "0.6.6", default-features = false }
+zip = { version = "0.6.6" }
tera = "1.19.1"
lazy_static = "1.4.0"
serde = { version = "1.0.193", features = ["derive"] }
+reqwest = "0.11.22"
+regex = "1.10.2"
+base64 = "0.21.5"
+
+[dev-dependencies]
+tokio = { version = "1.35.1", features = ["macros"] }
diff --git a/xsender/resources/test/R-12345678901-01-F001-00000587.xml b/xsender/resources/test/R-12345678901-01-F001-00000587.xml
new file mode 100644
index 00000000..31877a25
--- /dev/null
+++ b/xsender/resources/test/R-12345678901-01-F001-00000587.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+ VKY1IcYW+H/VLoYyShq8xvzea6YGCg6VOR7X4ZeA+hz/z9PZdkDPzN4e83PUOnRoBKnj3DDqZ0N9d6OSWAfaag==
+
+
+ *Private key 'BetaPublicCert' not up**Named certificate 'BetaPrivateKey' not up**Named certificate 'BetaPrivateKey' not up**Named certificate 'BetaPrivateKey' not up*15374531431912018-09-20T09:16:2500:00:002018-09-2010:19:03SignSUNAT20131312955SUNAT#SignSUNAT2013131295512345678901F001-000005870La Factura numero F001-00000587, ha sido aceptadaF001-000005876-10414774453
\ No newline at end of file
diff --git a/xsender/resources/test/R-20220557805-01-F001-22Openubl.xml b/xsender/resources/test/R-20220557805-01-F001-22Openubl.xml
new file mode 100644
index 00000000..36f660b3
--- /dev/null
+++ b/xsender/resources/test/R-20220557805-01-F001-22Openubl.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+ S67rWjkW4fu/JwOm8yKOO6A/vc+is7qOkh0SOrOXIvqaPid5hSr09GAV11VDkZn+fd9e3z5RDiJQKu1+8NyhpA==
+
+
+ *Private key 'BetaPublicCert' not up**Named certificate 'BetaPrivateKey' not up**Named certificate 'BetaPrivateKey' not up**Named certificate 'BetaPrivateKey' not up*16097397490912021-01-03T23:54:1700:00:002021-01-0323:55:49SignSUNAT20131312955SUNAT#SignSUNAT4252 - El dato ingresado como atributo @listName es incorrecto. - INFO: 4252 (nodo: "cbc:InvoiceTypeCode/listName" valor: "SUNAT:Identificador de Tipo de Documento")4255 - El dato ingresado como atributo @schemeName es incorrecto. - INFO: 4255 (nodo: "cbc:ID/schemeName" valor: "SUNAT:Identificador de Documento de Identidad")3030 - El XML no contiene el tag o no existe información del código de local anexo del emisor - INFO: 3030 (nodo: "/" valor: "")4255 - El dato ingresado como atributo @schemeName es incorrecto. - INFO: 4255 (nodo: "cbc:ID/schemeName" valor: "SUNAT:Identificador de Documento de Identidad")4252 - El dato ingresado como atributo @listName es incorrecto. - Error en la linea: 1: 4252 (nodo: "cbc:PriceTypeCode/listName" valor: "SUNAT:Indicador de Tipo de Precio")4252 - El dato ingresado como atributo @listName es incorrecto. - Error en la linea: 1 Tributo: 1000: 4252 (nodo: "cbc:TaxExemptionReasonCode/listName" valor: "SUNAT:Codigo de Tipo de Afectacion del IGV")4252 - El dato ingresado como atributo @listName es incorrecto. - Error en la linea: 2: 4252 (nodo: "cbc:PriceTypeCode/listName" valor: "SUNAT:Indicador de Tipo de Precio")4252 - El dato ingresado como atributo @listName es incorrecto. - Error en la linea: 2 Tributo: 1000: 4252 (nodo: "cbc:TaxExemptionReasonCode/listName" valor: "SUNAT:Codigo de Tipo de Afectacion del IGV")2013131295520220557805F001-220La Factura numero F001-22, ha sido aceptadaF001-226-20601814120
\ No newline at end of file
diff --git a/xsender/resources/test/bill_service_response_fault.xml b/xsender/resources/test/bill_service_response_fault.xml
new file mode 100644
index 00000000..9ec1dea1
--- /dev/null
+++ b/xsender/resources/test/bill_service_response_fault.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+ soap-env:Client.0111
+ No tiene el perfil para enviar comprobantes electronicos - Detalle: Rejected by policy.
+
+
+
\ No newline at end of file
diff --git a/xsender/resources/test/bill_service_response_ok.xml b/xsender/resources/test/bill_service_response_ok.xml
new file mode 100644
index 00000000..8674c1ff
--- /dev/null
+++ b/xsender/resources/test/bill_service_response_ok.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ UEsDBBQAAgAIAEwklVcAAAAAAgAAAAAAAAAGAAAAZHVtbXkvAwBQSwMEFAACAAgATCSVVwwGkuIxBAAAGQ0AABsAAABSLTEyMzQ1Njc4OTEyLTAxLUYwMDEtMS54bWy1V2FP2zwQ/r5fEZUPk6Y3OElbWKPQqVDGsgFitDC0byY52uhN7WA7peXXv+e4SdMStHbSq/LBuXvuubvHZ1sEXxaz1JqDkAlnJy330GlZwCIeJ2xy0robf7U/t770PwRU+IMsS5OIKgTegsw4k2BhMJMnrVwwn1OZSJ/RGUhfZhAlTyuwnz+mvoymMKP+QsZ+yOY8icD2Wibcp2JPhoZK1mywUHvSnfHZjLPzhQKmVcBPpASm5Jo0eoz+ivQU4VEjIf07wsFkImBCFTSRxrgVU6Uyn5CXl5fDl/YhFxPiOY5DnB5BTCyTyUGJlpxmFd4kkofo0vYiUC8IsDmkPANSJcHkVRgsZKoKsDZLm7LYVgn2UiUp+5Q5o+rdPjMQeb3ZkUY39eqWxIv3enXJw9XlqKAqscgCi6yhaHTkKRU2egVIvfmy1Q9wgvy708tqIGQ55g0+Y6nNDsOV6gejZIId5KI6IjvsCx4zHQZxyJ54/4NlBWeUcYY6pclrodUVqCmPrUE64SJR09m7EriOpsW+IjtyO+zgF6L1AGkNW6TgrircmdTplLXaMy7gQEhqyyntut6K8haeQODtAdbdbajlQiOax4Iy+cTFTBpD3fTHtBsSlcMY27Ks3qTek3QXgZCQbFceDJMJSLWnYqjIQV2niueepjn0H16PRPsBEvXt+ntGw3m4hMvrQXZ+OoB0zKYv98+/w2V4/hS/dm/uur+PB/C8OLvoEX41ypxzGf5KLn9cPF9E38/vczn8OVVikRz9PDkJSD2L3h9SbRCOGtmctfpEmIhPNyKZ4+mz/oWl9fEUFL3Bo4rXGQj10WJcWXn2ydDUooIfsCw4g4eu0xtSRc1KR5kzj8zXeA3EVrQ2rfhNQmSo8W8HF2yhlDmIEYiEpnWLJt6fvhZbcBne63z2CGJ/to3oeoKyXLJWhlRqrXXEdfOdQt5ePm9Msh/gW6VN9+ZND4d979AJyBtrgTvLpeKz1e2CRreEbjsKtAYcO20XJ/mo2z72DLTy6iaHeos8x+3Zrmd7nbHj+MXfClpB1hFjfC76DbDCXsDKN37F7bULbtdgN5wbcEPc8dsd3+tuglfcNPJrqq960ZbR3fVgXOuuAnKxvKFCLY2tWIYxbk71mlU0qEEbf16v210TkfejSoeZQh1QrGqVGA/ZQpL3isPDnyiaVg0OlKLRdFZMkvbrkRGMpus7wUzObdg/2NJA20yihiDyp2RkW2f9CSwG8f9ISRoT3EIEyXznnK7X7nSPjj/3XG/nnA0phjzKtQrl4JW1VF/FUK60xBRf8dWwq8le2zcG+4zHONibE13YCtQQZCSSrKjrklpfaYSyU4thHYJbJsE/1pRaMom5RSPIFI2pYavHlj3VC1+3szEzW4VXUjXBjU5JlqB9x704wvNe/fbZjY0spHk/SPN/Nv3/AFBLAQIAABQAAgAIAEwklVcAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAABkdW1teS9QSwECAAAUAAIACABMJJVXDAaS4jEEAAAZDQAAGwAAAAAAAAABAAAAAAAmAAAAUi0xMjM0NTY3ODkxMi0wMS1GMDAxLTEueG1sUEsFBgAAAAACAAIAfQAAAJAEAAAAAA==
+
+
+
\ No newline at end of file
diff --git a/xsender/resources/test/bill_service_response_ticket.xml b/xsender/resources/test/bill_service_response_ticket.xml
new file mode 100644
index 00000000..8b16984a
--- /dev/null
+++ b/xsender/resources/test/bill_service_response_ticket.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ 1703154974517
+
+
+
\ No newline at end of file
diff --git a/xsender/resources/test/get_status_response_ok.xml b/xsender/resources/test/get_status_response_ok.xml
new file mode 100644
index 00000000..14f432f9
--- /dev/null
+++ b/xsender/resources/test/get_status_response_ok.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+ UEsDBBQAAgAIAK07lVcAAAAAAgAAAAAAAAAGAAAAZHVtbXkvAwBQSwMEFAACAAgArTuVV2PumuIyBAAACA0AAB8AAABSLTEyMzQ1Njc4OTEyLVJBLTIwMjAwMzI4LTEueG1stVdRT+M4EH7fXxGVh5X2LjhJoYUoZFUo3FULPba0gPbNJEMbLrFzttMWfv2OkyZNS9C2K53gwZn55puZz2MbvK/LJDbmIGTE2VnLPrRaBrCAhxGbnrUm4yvzpPXV/+RR4fbSNI4CqhA4AplyJsHAYCbPWplgLqcyki6jCUhXphBEzyuwmz3FrgxmkFB3KUN3wOY8CsB0WkW4S8WeDA2VrNlgqfaku+BJwtnlUgHTKuAnUgJTck0aPAW/RXqO8KCRkP4eYW86FTClCppIQ9yKmVKpS8hisThctA+5mBLHsixinRLEhDKaHpRoyWla4YtE8hBd2p4H6gUBNoeYp0CqJJi8CoOljFUO1mZpUhaaKsJeqiRlnzJjVH3YZwoiqzd7p9FNvdol8fKjXm3yeHN9l1OVWGSBZdpQNDqymAoTvQKk3nzZ8j2cIHdyfl0NhCzHvMFXWGqzw3ClfO8ummIHmaiOyA77gsdMh0E4YM/c/2QY3gVlnKFOcfSWa3UDasZDoxdPuYjULPlQAtvStNhXYAb2ETt4QLQeIK1hi+TcVYU7k1pHZa1mwgUcCElNOaPHtrOiHMEzCLw9wJiMBlouNKJ5LCiTz1wksjDUTb9MuyFROYyhKcvqi9R7ku4iEBKS7cq9fjQFqfZUDBU5qOtU8dzTOAP/hS7D/iWx5peB9eMvMumyf9KnvmN/z0bf528L/kYerkZXr7Ph+ViSm5voPusOX5aTq6Pn3n8PCfs7kT+GycPL203YW9BR6jyO/7gNF2dnHqln0ftDqg3CUSObs1afiCLiy62I5nj6jH/h1fh8Dore4lHF6wyE+mwwrows/VLQ1KK8b/Cac3qPx9ZpnyparHRUceaReYjXQGgEa9OKv0iIDDX+7eCcbSBlBuIORETjukUT709fi825Ct5hljyB2J9tI7qeoCyXrJUhlVprHXHdfKeQ95fPO5P0PXyrtOm+eNMHfd85tDzyzprjLjKpeLK6XdBol9BtR47WgK7Vtjt2t9M5alt2ga3cukvsC0jte4yvgW9Zbv67wlf2HFY+4TrSdyynbdqO6ay4N5wb8IK46zqnrtPZBK+4aeDWRF1Vqi13k2FvXKu9AnLxekuFei1s+XIQovbVY1XROJbdxh/n9Ph4TUQ+jiodxZDpgHxVq6TwkC0k+ag4PNuRonHVYE8pGsySfFC0X0+EYDReH/liMEYD/2BLA20rEjUEkV8lI9s6609gIYj/R0rSmGAEAUTznXPaTvvouNM9ObWdnXM2pOjzINMqlINX1lJ95UO50hJTjHomDrhltZ0TsxrwtXtjvi94iPO9Odi5LUf1QQYiSvPyrqmBT1em/1AI8NsIwXiiL9TYyPanMaOGjEJu0ABSRUNaUNeJyj7rzaxb3Jij5mYqFZuiCgmjNEL7jtvUMffZnQ1q0rw/pPkfGf8nUEsBAgAAFAACAAgArTuVVwAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAGR1bW15L1BLAQIAABQAAgAIAK07lVdj7priMgQAAAgNAAAfAAAAAAAAAAEAAAAAACYAAABSLTEyMzQ1Njc4OTEyLVJBLTIwMjAwMzI4LTEueG1sUEsFBgAAAAACAAIAgQAAAJUEAAAAAA==
+ 0
+
+
+
+
\ No newline at end of file
diff --git a/xsender/src/analyzer.rs b/xsender/src/analyzer.rs
index a7a87fe4..d2c4cd41 100644
--- a/xsender/src/analyzer.rs
+++ b/xsender/src/analyzer.rs
@@ -1,147 +1,153 @@
-use crate::metadata::Metadata;
-
-pub struct DocumentType {}
-
-impl DocumentType {
- const INVOICE: &'static str = "Invoice";
- const CREDIT_NOTE: &'static str = "CreditNote";
- const DEBIT_NOTE: &'static str = "DebitNote";
- const VOIDED_DOCUMENTS: &'static str = "VoidedDocuments";
- const SUMMARY_DOCUMENTS: &'static str = "SummaryDocuments";
- const DESPATCH_ADVICE: &'static str = "DespatchAdvise";
- const PERCEPTION: &'static str = "Perception";
- const RETENTION: &'static str = "Retention";
-}
-
-pub struct Catalog1 {}
-
-#[allow(dead_code)]
-impl Catalog1 {
- const FACTURA: &'static str = "01";
- const BOLETA: &'static str = "03";
- const NOTA_CREDITO: &'static str = "07";
- const NOTA_DEBITO: &'static str = "08";
- const GUIA_REMISION_REMITENTE: &'static str = "09";
- const RETENCION: &'static str = "20";
- const PERCEPCION: &'static str = "40";
-}
-
-pub struct DeliveryUrls {
- pub invoice: String,
- pub perception_retention: String,
- pub despatch: String,
-}
-
-#[derive(Debug, PartialEq)]
-pub enum Delivery {
- SOAP(String, SoapAction),
- REST(String, RestAction),
-}
-
-#[derive(Debug, PartialEq)]
-pub enum SoapAction {
- SendBill,
- SendSummary,
- SendPack,
- GetStatus,
-}
-
-#[derive(Debug, PartialEq)]
-pub enum RestAction {
- SendDocument,
- VerifyTicket,
-}
-
-pub trait Analyze {
- fn target(&self, urls: &DeliveryUrls) -> Option;
- fn ticket(&self, urls: &DeliveryUrls) -> Option;
-}
-
-impl Analyze for Metadata {
- fn target(&self, urls: &DeliveryUrls) -> Option {
- match self.document_type.as_str() {
- DocumentType::INVOICE | DocumentType::CREDIT_NOTE | DocumentType::DEBIT_NOTE => {
- Some(Delivery::SOAP(urls.invoice.clone(), SoapAction::SendBill))
- }
- DocumentType::SUMMARY_DOCUMENTS => Some(Delivery::SOAP(
- urls.invoice.clone(),
- SoapAction::SendSummary,
- )),
- DocumentType::VOIDED_DOCUMENTS => {
- match self.voided_line_document_type_code.as_deref() {
- Some(Catalog1::RETENCION) | Some(Catalog1::PERCEPCION) => Some(Delivery::SOAP(
- urls.perception_retention.clone(),
- SoapAction::SendSummary,
- )),
- Some(Catalog1::GUIA_REMISION_REMITENTE) => None,
- Some(_) => Some(Delivery::SOAP(
- urls.invoice.clone(),
- SoapAction::SendSummary,
- )),
- _ => None,
- }
- }
- DocumentType::PERCEPTION | DocumentType::RETENTION => Some(Delivery::SOAP(
+use log::warn;
+
+use crate::global::{
+ BOLETA_SERIE_REGEX, FACTURA_SERIE_REGEX, GUIA_REMISION_REMITENTE_SERIE_REGEX,
+ GUIA_REMISION_TRANSPORTISTA_SERIE_REGEX,
+};
+use crate::models::Urls;
+use crate::models::{
+ Catalog1, DocumentType, RestFileTargetAction, SendFileTarget, SoapFileTargetAction,
+ VerifyTicketTarget,
+};
+
+pub fn send_file_target(
+ document_type: &str,
+ voided_line_document_type_code: &Option,
+ urls: &Urls,
+) -> Option {
+ match document_type {
+ DocumentType::INVOICE | DocumentType::CREDIT_NOTE | DocumentType::DEBIT_NOTE => Some(
+ SendFileTarget::Soap(urls.invoice.clone(), SoapFileTargetAction::Bill),
+ ),
+ DocumentType::SUMMARY_DOCUMENTS => Some(SendFileTarget::Soap(
+ urls.invoice.clone(),
+ SoapFileTargetAction::Summary,
+ )),
+ DocumentType::VOIDED_DOCUMENTS => match voided_line_document_type_code.as_deref() {
+ Some(Catalog1::RETENCION) | Some(Catalog1::PERCEPCION) => Some(SendFileTarget::Soap(
urls.perception_retention.clone(),
- SoapAction::SendBill,
+ SoapFileTargetAction::Summary,
)),
- DocumentType::DESPATCH_ADVICE => Some(Delivery::REST(
- urls.despatch.clone(),
- RestAction::SendDocument,
+ Some(Catalog1::GUIA_REMISION_REMITENTE) => None,
+ Some(_) => Some(SendFileTarget::Soap(
+ urls.invoice.clone(),
+ SoapFileTargetAction::Summary,
)),
_ => None,
- }
+ },
+ DocumentType::PERCEPTION | DocumentType::RETENTION => Some(SendFileTarget::Soap(
+ urls.perception_retention.clone(),
+ SoapFileTargetAction::Bill,
+ )),
+ DocumentType::DESPATCH_ADVICE => Some(SendFileTarget::Rest(
+ urls.despatch.clone(),
+ RestFileTargetAction::SendDocument,
+ )),
+ _ => None,
}
+}
- fn ticket(&self, urls: &DeliveryUrls) -> Option {
- match self.document_type.as_str() {
- DocumentType::VOIDED_DOCUMENTS => {
- match self.voided_line_document_type_code.as_deref() {
- Some(Catalog1::RETENCION) | Some(Catalog1::PERCEPCION) => Some(Delivery::SOAP(
- urls.perception_retention.clone(),
- SoapAction::GetStatus,
- )),
- Some(Catalog1::GUIA_REMISION_REMITENTE) => None,
- Some(_) => Some(Delivery::SOAP(urls.invoice.clone(), SoapAction::GetStatus)),
- _ => None,
- }
+pub fn verify_ticket_target(
+ document_type: &str,
+ voided_line_document_type_code: &Option,
+ urls: &Urls,
+) -> Option {
+ match document_type {
+ DocumentType::VOIDED_DOCUMENTS => match voided_line_document_type_code.as_deref() {
+ Some(Catalog1::RETENCION) | Some(Catalog1::PERCEPCION) => {
+ Some(VerifyTicketTarget::Soap(urls.perception_retention.clone()))
}
- DocumentType::SUMMARY_DOCUMENTS => {
- Some(Delivery::SOAP(urls.invoice.clone(), SoapAction::GetStatus))
- }
- DocumentType::DESPATCH_ADVICE => Some(Delivery::REST(
- urls.despatch.clone(),
- RestAction::VerifyTicket,
- )),
+ Some(Catalog1::GUIA_REMISION_REMITENTE) => None,
+ Some(_) => Some(VerifyTicketTarget::Soap(urls.invoice.clone())),
_ => None,
+ },
+ DocumentType::SUMMARY_DOCUMENTS => Some(VerifyTicketTarget::Soap(urls.invoice.clone())),
+ DocumentType::DESPATCH_ADVICE => Some(VerifyTicketTarget::Rest(urls.despatch.clone())),
+ _ => None,
+ }
+}
+
+pub fn filename_formatted_without_extension(
+ document_type: &str,
+ document_id: &str,
+ ruc: &str,
+) -> Option {
+ match document_type {
+ DocumentType::INVOICE => {
+ if FACTURA_SERIE_REGEX.is_match(document_id) {
+ Some(format!("{ruc}-{}-{document_id}", Catalog1::FACTURA))
+ } else if BOLETA_SERIE_REGEX.is_match(document_id) {
+ Some(format!("{ruc}-{}-{document_id}", Catalog1::BOLETA))
+ } else {
+ warn!("Could not build filename from Invoice");
+ None
+ }
+ }
+ DocumentType::CREDIT_NOTE => {
+ Some(format!("{ruc}-{}-{document_id}", Catalog1::NOTA_CREDITO))
+ }
+ DocumentType::DEBIT_NOTE => Some(format!("{ruc}-{}-{document_id}", Catalog1::NOTA_DEBITO)),
+ DocumentType::VOIDED_DOCUMENTS | DocumentType::SUMMARY_DOCUMENTS => {
+ Some(format!("{ruc}-{document_id}"))
}
+ DocumentType::PERCEPTION => Some(format!("{ruc}-{}-{document_id}", Catalog1::PERCEPCION)),
+ DocumentType::RETENTION => Some(format!("{ruc}-{}-{document_id}", Catalog1::RETENCION)),
+ DocumentType::DESPATCH_ADVICE => {
+ if GUIA_REMISION_REMITENTE_SERIE_REGEX.is_match(document_id) {
+ Some(format!(
+ "{ruc}-{}-{document_id}",
+ Catalog1::GUIA_REMISION_REMITENTE
+ ))
+ } else if GUIA_REMISION_TRANSPORTISTA_SERIE_REGEX.is_match(document_id) {
+ Some(format!(
+ "{ruc}-{}-{document_id}",
+ Catalog1::GUIA_REMISION_TRANSPORTISTA
+ ))
+ } else {
+ warn!("Could not build filename from DespatchAdvice");
+ None
+ }
+ }
+ _ => None,
}
}
#[cfg(test)]
mod tests {
- use crate::analyzer::{
- Analyze, Catalog1, Delivery, DeliveryUrls, DocumentType, RestAction, SoapAction,
+ use crate::analyzer::{send_file_target, verify_ticket_target};
+ use crate::models::{
+ Catalog1, DocumentType, RestFileTargetAction, SendFileTarget, SoapFileTargetAction, Urls,
+ VerifyTicketTarget,
};
- use crate::metadata::Metadata;
+ use crate::ubl_file::UblMetadata;
- #[test]
- fn unknow_document_type() {
- let delivery_urls: DeliveryUrls = DeliveryUrls {
+ lazy_static::lazy_static! {
+ pub static ref DELIVERY_URLS: Urls = Urls {
invoice: String::from("https://invoice"),
perception_retention: String::from("https://perception_retention"),
despatch: String::from("https://despatch"),
};
+ }
- let metadata = Metadata {
+ #[test]
+ fn unknown_document_type() {
+ let metadata = UblMetadata {
document_type: String::from("Unknown"),
- document_id: Some(String::from("F001-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("F001-1"),
+ ruc: String::from("123456789012"),
voided_line_document_type_code: None,
};
- let document_delivery = metadata.target(&delivery_urls);
- let ticket_delivery = metadata.ticket(&delivery_urls);
+ let document_delivery = send_file_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ );
+ let ticket_delivery = verify_ticket_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ );
assert_eq!(document_delivery, None);
assert_eq!(ticket_delivery, None);
@@ -149,213 +155,277 @@ mod tests {
#[test]
fn invoice() {
- let delivery_urls: DeliveryUrls = DeliveryUrls {
- invoice: String::from("https://invoice"),
- perception_retention: String::from("https://perception_retention"),
- despatch: String::from("https://despatch"),
- };
-
- let metadata = Metadata {
+ let metadata = UblMetadata {
document_type: String::from(DocumentType::INVOICE),
- document_id: Some(String::from("F001-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("F001-1"),
+ ruc: String::from("123456789012"),
voided_line_document_type_code: None,
};
- let document_delivery = metadata.target(&delivery_urls).unwrap();
- let ticket_delivery = metadata.ticket(&delivery_urls);
+ let document_delivery = send_file_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
+ let ticket_delivery = verify_ticket_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ );
assert_eq!(
document_delivery,
- Delivery::SOAP(String::from("https://invoice"), SoapAction::SendBill)
+ SendFileTarget::Soap(String::from("https://invoice"), SoapFileTargetAction::Bill)
);
assert_eq!(ticket_delivery, None);
}
#[test]
fn credit_note() {
- let delivery_urls: DeliveryUrls = DeliveryUrls {
- invoice: String::from("https://invoice"),
- perception_retention: String::from("https://perception_retention"),
- despatch: String::from("https://despatch"),
- };
-
- let metadata = Metadata {
+ let metadata = UblMetadata {
document_type: String::from(DocumentType::CREDIT_NOTE),
- document_id: Some(String::from("FC01-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("FC01-1"),
+ ruc: String::from("123456789012"),
voided_line_document_type_code: None,
};
- let document_delivery = metadata.target(&delivery_urls).unwrap();
- let ticket_delivery = metadata.ticket(&delivery_urls);
+ let document_delivery = send_file_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
+ let ticket_delivery = verify_ticket_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ );
assert_eq!(
document_delivery,
- Delivery::SOAP(String::from("https://invoice"), SoapAction::SendBill)
+ SendFileTarget::Soap(String::from("https://invoice"), SoapFileTargetAction::Bill)
);
assert_eq!(ticket_delivery, None);
}
#[test]
fn debit_note() {
- let delivery_urls: DeliveryUrls = DeliveryUrls {
- invoice: String::from("https://invoice"),
- perception_retention: String::from("https://perception_retention"),
- despatch: String::from("https://despatch"),
- };
-
- let metadata = Metadata {
+ let metadata = UblMetadata {
document_type: String::from(DocumentType::DEBIT_NOTE),
- document_id: Some(String::from("FD01-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("FD01-1"),
+ ruc: String::from("123456789012"),
voided_line_document_type_code: None,
};
- let document_delivery = metadata.target(&delivery_urls).unwrap();
- let ticket_delivery = metadata.ticket(&delivery_urls);
+ let document_delivery = send_file_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
+ let ticket_delivery = verify_ticket_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ );
assert_eq!(
document_delivery,
- Delivery::SOAP(String::from("https://invoice"), SoapAction::SendBill)
+ SendFileTarget::Soap(String::from("https://invoice"), SoapFileTargetAction::Bill)
);
assert_eq!(ticket_delivery, None);
}
#[test]
fn voided_documents() {
- let delivery_urls: DeliveryUrls = DeliveryUrls {
- invoice: String::from("https://invoice"),
- perception_retention: String::from("https://perception_retention"),
- despatch: String::from("https://despatch"),
- };
-
// Invoice
- let baja_invoice = Metadata {
+ let baja_invoice = UblMetadata {
voided_line_document_type_code: Some(String::from(Catalog1::FACTURA)),
document_type: String::from(DocumentType::VOIDED_DOCUMENTS),
- document_id: Some(String::from("RA-20200328-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("RA-20200328-1"),
+ ruc: String::from("123456789012"),
};
- let document_delivery = baja_invoice.target(&delivery_urls).unwrap();
- let ticket_delivery = baja_invoice.ticket(&delivery_urls).unwrap();
+ let document_delivery = send_file_target(
+ &baja_invoice.document_type,
+ &baja_invoice.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
+ let ticket_delivery = verify_ticket_target(
+ &baja_invoice.document_type,
+ &baja_invoice.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
assert_eq!(
document_delivery,
- Delivery::SOAP(String::from("https://invoice"), SoapAction::SendSummary)
+ SendFileTarget::Soap(
+ String::from("https://invoice"),
+ SoapFileTargetAction::Summary,
+ )
);
assert_eq!(
ticket_delivery,
- Delivery::SOAP(String::from("https://invoice"), SoapAction::GetStatus)
+ VerifyTicketTarget::Soap(String::from("https://invoice"))
);
// CreditNote
- let baja_credit_note = Metadata {
+ let baja_credit_note = UblMetadata {
voided_line_document_type_code: Some(String::from(Catalog1::NOTA_CREDITO)),
document_type: String::from(DocumentType::VOIDED_DOCUMENTS),
- document_id: Some(String::from("RA-20200328-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("RA-20200328-1"),
+ ruc: String::from("123456789012"),
};
- let document_delivery = baja_credit_note.target(&delivery_urls).unwrap();
- let ticket_delivery = baja_credit_note.ticket(&delivery_urls).unwrap();
+ let document_delivery = send_file_target(
+ &baja_credit_note.document_type,
+ &baja_credit_note.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
+ let ticket_delivery = verify_ticket_target(
+ &baja_credit_note.document_type,
+ &baja_credit_note.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
assert_eq!(
document_delivery,
- Delivery::SOAP(String::from("https://invoice"), SoapAction::SendSummary)
+ SendFileTarget::Soap(
+ String::from("https://invoice"),
+ SoapFileTargetAction::Summary,
+ )
);
assert_eq!(
ticket_delivery,
- Delivery::SOAP(String::from("https://invoice"), SoapAction::GetStatus)
+ VerifyTicketTarget::Soap(String::from("https://invoice"))
);
// DebitNote
- let baja_debit_note = Metadata {
+ let baja_debit_note = UblMetadata {
voided_line_document_type_code: Some(String::from(Catalog1::NOTA_DEBITO)),
document_type: String::from(DocumentType::VOIDED_DOCUMENTS),
- document_id: Some(String::from("RA-20200328-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("RA-20200328-1"),
+ ruc: String::from("123456789012"),
};
- let document_delivery = baja_debit_note.target(&delivery_urls).unwrap();
- let ticket_delivery = baja_debit_note.ticket(&delivery_urls).unwrap();
+ let document_delivery = send_file_target(
+ &baja_debit_note.document_type,
+ &baja_debit_note.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
+ let ticket_delivery = verify_ticket_target(
+ &baja_debit_note.document_type,
+ &baja_debit_note.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
assert_eq!(
document_delivery,
- Delivery::SOAP(String::from("https://invoice"), SoapAction::SendSummary)
+ SendFileTarget::Soap(
+ String::from("https://invoice"),
+ SoapFileTargetAction::Summary,
+ )
);
assert_eq!(
ticket_delivery,
- Delivery::SOAP(String::from("https://invoice"), SoapAction::GetStatus)
+ VerifyTicketTarget::Soap(String::from("https://invoice"))
);
// Percepcion
- let baja_percepcion = Metadata {
+ let baja_percepcion = UblMetadata {
voided_line_document_type_code: Some(String::from(Catalog1::PERCEPCION)),
document_type: String::from(DocumentType::VOIDED_DOCUMENTS),
- document_id: Some(String::from("RA-20200328-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("RA-20200328-1"),
+ ruc: String::from("123456789012"),
};
- let document_delivery = baja_percepcion.target(&delivery_urls).unwrap();
- let ticket_delivery = baja_percepcion.ticket(&delivery_urls).unwrap();
+ let document_delivery = send_file_target(
+ &baja_percepcion.document_type,
+ &baja_percepcion.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
+ let ticket_delivery = verify_ticket_target(
+ &baja_percepcion.document_type,
+ &baja_percepcion.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
assert_eq!(
document_delivery,
- Delivery::SOAP(
+ SendFileTarget::Soap(
String::from("https://perception_retention"),
- SoapAction::SendSummary
+ SoapFileTargetAction::Summary,
)
);
assert_eq!(
ticket_delivery,
- Delivery::SOAP(
- String::from("https://perception_retention"),
- SoapAction::GetStatus
- )
+ VerifyTicketTarget::Soap(String::from("https://perception_retention"))
);
// Retencion
- let baja_retencion = Metadata {
+ let baja_retencion = UblMetadata {
document_type: String::from(DocumentType::VOIDED_DOCUMENTS),
- document_id: Some(String::from("RA-20200328-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("RA-20200328-1"),
+ ruc: String::from("123456789012"),
voided_line_document_type_code: Some(String::from(Catalog1::RETENCION)),
};
- let document_delivery = baja_retencion.target(&delivery_urls).unwrap();
- let ticket_delivery = baja_retencion.ticket(&delivery_urls).unwrap();
+ let document_delivery = send_file_target(
+ &baja_retencion.document_type,
+ &baja_retencion.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
+ let ticket_delivery = verify_ticket_target(
+ &baja_retencion.document_type,
+ &baja_retencion.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
assert_eq!(
document_delivery,
- Delivery::SOAP(
+ SendFileTarget::Soap(
String::from("https://perception_retention"),
- SoapAction::SendSummary
+ SoapFileTargetAction::Summary,
)
);
assert_eq!(
ticket_delivery,
- Delivery::SOAP(
- String::from("https://perception_retention"),
- SoapAction::GetStatus
- )
+ VerifyTicketTarget::Soap(String::from("https://perception_retention"))
);
// Guia
- let baja_guia = Metadata {
+ let baja_guia = UblMetadata {
document_type: String::from(DocumentType::VOIDED_DOCUMENTS),
- document_id: Some(String::from("RA-20200328-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("RA-20200328-1"),
+ ruc: String::from("123456789012"),
voided_line_document_type_code: Some(String::from(Catalog1::GUIA_REMISION_REMITENTE)),
};
- let document_delivery = baja_guia.target(&delivery_urls);
- let ticket_delivery = baja_guia.ticket(&delivery_urls);
+ let document_delivery = send_file_target(
+ &baja_guia.document_type,
+ &baja_guia.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ );
+ let ticket_delivery = verify_ticket_target(
+ &baja_guia.document_type,
+ &baja_guia.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ );
assert_eq!(document_delivery, None);
assert_eq!(ticket_delivery, None);
@@ -363,55 +433,65 @@ mod tests {
#[test]
fn summary_documents() {
- let delivery_urls: DeliveryUrls = DeliveryUrls {
- invoice: String::from("https://invoice"),
- perception_retention: String::from("https://perception_retention"),
- despatch: String::from("https://despatch"),
- };
-
- let metadata = Metadata {
+ let metadata = UblMetadata {
document_type: String::from(DocumentType::SUMMARY_DOCUMENTS),
- document_id: Some(String::from("S001-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("S001-1"),
+ ruc: String::from("123456789012"),
voided_line_document_type_code: None,
};
- let document_delivery = metadata.target(&delivery_urls).unwrap();
- let ticket_delivery = metadata.ticket(&delivery_urls).unwrap();
+ let document_delivery = send_file_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
+ let ticket_delivery = verify_ticket_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
assert_eq!(
document_delivery,
- Delivery::SOAP(String::from("https://invoice"), SoapAction::SendSummary)
+ SendFileTarget::Soap(
+ String::from("https://invoice"),
+ SoapFileTargetAction::Summary,
+ )
);
assert_eq!(
ticket_delivery,
- Delivery::SOAP(String::from("https://invoice"), SoapAction::GetStatus)
+ VerifyTicketTarget::Soap(String::from("https://invoice"))
);
}
#[test]
fn perception() {
- let delivery_urls: DeliveryUrls = DeliveryUrls {
- invoice: String::from("https://invoice"),
- perception_retention: String::from("https://perception_retention"),
- despatch: String::from("https://despatch"),
- };
-
- let metadata = Metadata {
+ let metadata = UblMetadata {
document_type: String::from(DocumentType::PERCEPTION),
- document_id: Some(String::from("S001-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("S001-1"),
+ ruc: String::from("123456789012"),
voided_line_document_type_code: None,
};
- let document_delivery = metadata.target(&delivery_urls).unwrap();
- let ticket_delivery = metadata.ticket(&delivery_urls);
+ let document_delivery = send_file_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
+ let ticket_delivery = verify_ticket_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ );
assert_eq!(
document_delivery,
- Delivery::SOAP(
+ SendFileTarget::Soap(
String::from("https://perception_retention"),
- SoapAction::SendBill
+ SoapFileTargetAction::Bill,
)
);
assert_eq!(ticket_delivery, None);
@@ -419,27 +499,30 @@ mod tests {
#[test]
fn retention() {
- let delivery_urls: DeliveryUrls = DeliveryUrls {
- invoice: String::from("https://invoice"),
- perception_retention: String::from("https://perception_retention"),
- despatch: String::from("https://despatch"),
- };
-
- let metadata = Metadata {
+ let metadata = UblMetadata {
document_type: String::from(DocumentType::RETENTION),
- document_id: Some(String::from("R001-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("R001-1"),
+ ruc: String::from("123456789012"),
voided_line_document_type_code: None,
};
- let document_delivery = metadata.target(&delivery_urls).unwrap();
- let ticket_delivery = metadata.ticket(&delivery_urls);
+ let document_delivery = send_file_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
+ let ticket_delivery = verify_ticket_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ );
assert_eq!(
document_delivery,
- Delivery::SOAP(
+ SendFileTarget::Soap(
String::from("https://perception_retention"),
- SoapAction::SendBill
+ SoapFileTargetAction::Bill,
)
);
assert_eq!(ticket_delivery, None);
@@ -447,29 +530,36 @@ mod tests {
#[test]
fn despatch_advise() {
- let delivery_urls: DeliveryUrls = DeliveryUrls {
- invoice: String::from("https://invoice"),
- perception_retention: String::from("https://perception_retention"),
- despatch: String::from("https://despatch"),
- };
-
- let metadata = Metadata {
+ let metadata = UblMetadata {
document_type: String::from(DocumentType::DESPATCH_ADVICE),
- document_id: Some(String::from("D001-1")),
- ruc: Some(String::from("123456789012")),
+ document_id: String::from("D001-1"),
+ ruc: String::from("123456789012"),
voided_line_document_type_code: None,
};
- let document_delivery = metadata.target(&delivery_urls).unwrap();
- let ticket_delivery = metadata.ticket(&delivery_urls).unwrap();
+ let document_delivery = send_file_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
+ let ticket_delivery = verify_ticket_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &DELIVERY_URLS,
+ )
+ .unwrap();
assert_eq!(
document_delivery,
- Delivery::REST(String::from("https://despatch"), RestAction::SendDocument)
+ SendFileTarget::Rest(
+ String::from("https://despatch"),
+ RestFileTargetAction::SendDocument,
+ )
);
assert_eq!(
ticket_delivery,
- Delivery::REST(String::from("https://despatch"), RestAction::VerifyTicket)
+ VerifyTicketTarget::Rest(String::from("https://despatch"))
);
}
}
diff --git a/xsender/src/client_sunat.rs b/xsender/src/client_sunat.rs
new file mode 100644
index 00000000..d588c1b2
--- /dev/null
+++ b/xsender/src/client_sunat.rs
@@ -0,0 +1,228 @@
+use std::str::FromStr;
+
+use crate::models::{
+ Credentials, RestFileTargetAction, SendFileTarget, SoapFileTargetAction, VerifyTicketTarget,
+};
+use crate::soap::cdr::{Cdr, CdrReadError};
+use crate::soap::envelope::{BodyData, EnvelopeData, FileData, ToStringXml};
+use crate::soap::send_file_response::{SendFileXmlResponse, SendFileXmlResponseFromStrError};
+use crate::soap::verify_ticket_response::{
+ VerifyTicketXmlResponse, VerifyTicketXmlResponseFromStrError,
+};
+use crate::zip_manager::{extract_cdr_from_base64_zip, ExtractCdrFromBase64ZipError};
+
+#[derive(Default)]
+pub struct ClientSUNAT {}
+
+pub struct File {
+ pub name: String,
+ pub base_64: String,
+}
+
+#[derive(Debug)]
+pub struct ErrorClientSUNAT {
+ pub kind: Layer,
+}
+
+#[derive(Debug)]
+pub enum Layer {
+ Templating(String),
+ Request(reqwest::Error),
+ ReadingResponse,
+}
+
+pub enum SendFileResponse {
+ Ok(String, Cdr),
+ Ticket(String),
+ Error(String, String),
+}
+
+pub enum VerifyTicketResponse {
+ Ok(String, Cdr, String),
+ Error(String, String),
+}
+
+impl ClientSUNAT {
+ pub async fn send_file(
+ &self,
+ target: &SendFileTarget,
+ file: &File,
+ credentials: &Credentials,
+ ) -> Result {
+ match &target {
+ SendFileTarget::Soap(url, action) => {
+ let envelope_body = match action {
+ SoapFileTargetAction::Bill | SoapFileTargetAction::Pack => {
+ BodyData::SendBill(FileData {
+ filename: file.name.clone(),
+ file_content: file.base_64.clone(),
+ })
+ }
+ SoapFileTargetAction::Summary => BodyData::SendSummary(FileData {
+ filename: file.name.clone(),
+ file_content: file.base_64.clone(),
+ }),
+ };
+
+ let envelope = EnvelopeData {
+ username: credentials.username.clone(),
+ password: credentials.password.clone(),
+ body: envelope_body,
+ };
+
+ let soap_action = match action {
+ SoapFileTargetAction::Bill => "urn:sendBill",
+ SoapFileTargetAction::Summary => "urn:sendSummary",
+ SoapFileTargetAction::Pack => "urn:sendPack",
+ };
+
+ let body = envelope.to_string_xml()?;
+
+ let body = reqwest::Client::new()
+ .post(url)
+ .body(body)
+ .header("Content-Type", "text/xml; charset=utf-8")
+ .header("SOAPAction", soap_action)
+ .send()
+ .await?
+ .text()
+ .await?;
+
+ let response_body = SendFileXmlResponse::from_str(&body)?;
+
+ let result = match response_body {
+ SendFileXmlResponse::Cdr(cdr_base64) => {
+ let cdr_xml = extract_cdr_from_base64_zip(&cdr_base64)?;
+ let cdr = Cdr::from_str(&cdr_xml)?;
+ SendFileResponse::Ok(cdr_base64, cdr)
+ }
+ SendFileXmlResponse::Ticket(ticket) => SendFileResponse::Ticket(ticket),
+ SendFileXmlResponse::Fault(code, description) => {
+ SendFileResponse::Error(code, description)
+ }
+ };
+
+ Ok(result)
+ }
+ SendFileTarget::Rest(url, action) => {
+ let path = match action {
+ RestFileTargetAction::SendDocument => "/comprobantes",
+ };
+ let _ = reqwest::Client::new()
+ .post(format!("{url}/{path}/{}", file.name))
+ .body("test")
+ .header("Content-Type", "application/json")
+ .send()
+ .await?
+ .text()
+ .await?;
+ Ok(SendFileResponse::Ticket("".to_string()))
+ }
+ }
+ }
+
+ pub async fn verify_ticket(
+ &self,
+ target: &VerifyTicketTarget,
+ ticket: &str,
+ credentials: &Credentials,
+ ) -> Result {
+ match &target {
+ VerifyTicketTarget::Soap(url) => {
+ let envelope = EnvelopeData {
+ username: credentials.username.clone(),
+ password: credentials.password.clone(),
+ body: BodyData::VerifyTicket(ticket.to_string()),
+ };
+
+ let body = envelope.to_string_xml()?;
+
+ let body = reqwest::Client::new()
+ .post(url)
+ .body(body)
+ .header("Content-Type", "text/xml; charset=utf-8")
+ .header("SOAPAction", "urn:getStatus")
+ .send()
+ .await?
+ .text()
+ .await?;
+
+ let response_body = VerifyTicketXmlResponse::from_str(&body)?;
+
+ match response_body {
+ VerifyTicketXmlResponse::Cdr(cdr_base64, status_code) => {
+ let cdr_xml = extract_cdr_from_base64_zip(&cdr_base64)?;
+ let cdr = Cdr::from_str(&cdr_xml)?;
+ let result = VerifyTicketResponse::Ok(cdr_base64, cdr, status_code);
+
+ Ok(result)
+ }
+ VerifyTicketXmlResponse::Fault(code, description) => {
+ let result = VerifyTicketResponse::Error(code, description);
+
+ Ok(result)
+ }
+ }
+ }
+ VerifyTicketTarget::Rest(url) => {
+ let _ = reqwest::Client::new()
+ .post(format!("{url}/ticket"))
+ .body("test")
+ .header("Content-Type", "application/json")
+ .send()
+ .await?
+ .text()
+ .await?;
+ Ok(VerifyTicketResponse::Error("".to_string(), "".to_string()))
+ }
+ }
+ }
+}
+
+impl From for ErrorClientSUNAT {
+ fn from(error: reqwest::Error) -> Self {
+ Self {
+ kind: Layer::Request(error),
+ }
+ }
+}
+
+impl From for ErrorClientSUNAT {
+ fn from(value: tera::Error) -> Self {
+ Self {
+ kind: Layer::Templating(value.to_string()),
+ }
+ }
+}
+
+impl From for ErrorClientSUNAT {
+ fn from(_: SendFileXmlResponseFromStrError) -> Self {
+ Self {
+ kind: Layer::ReadingResponse,
+ }
+ }
+}
+
+impl From for ErrorClientSUNAT {
+ fn from(_: VerifyTicketXmlResponseFromStrError) -> Self {
+ Self {
+ kind: Layer::ReadingResponse,
+ }
+ }
+}
+
+impl From for ErrorClientSUNAT {
+ fn from(_: ExtractCdrFromBase64ZipError) -> Self {
+ Self {
+ kind: Layer::ReadingResponse,
+ }
+ }
+}
+
+impl From for ErrorClientSUNAT {
+ fn from(_: CdrReadError) -> Self {
+ Self {
+ kind: Layer::ReadingResponse,
+ }
+ }
+}
diff --git a/xsender/src/file_sender.rs b/xsender/src/file_sender.rs
new file mode 100644
index 00000000..7bdce3a8
--- /dev/null
+++ b/xsender/src/file_sender.rs
@@ -0,0 +1,121 @@
+use base64::engine::general_purpose;
+use base64::Engine;
+use zip::result::ZipError;
+
+use crate::analyzer::{
+ filename_formatted_without_extension, send_file_target, verify_ticket_target,
+};
+use crate::client_sunat::{
+ ClientSUNAT, ErrorClientSUNAT, File, SendFileResponse, VerifyTicketResponse,
+};
+use crate::models::{Credentials, SendFileTarget, Urls, VerifyTicketTarget};
+use crate::ubl_file::{UblFile, UblMetadataError};
+use crate::zip_manager::create_zip;
+
+pub struct FileSender {
+ pub urls: Urls,
+ pub credentials: Credentials,
+}
+
+#[derive(Debug)]
+pub struct FileSenderError {
+ pub message: String,
+ pub client_error: Option,
+}
+
+pub struct SendFileResponseWrapper {
+ pub data: SendFileResponse,
+ pub send_file_target: SendFileTarget,
+ pub verify_ticket_target: Option,
+}
+
+impl FileSender {
+ pub async fn send_file(
+ &self,
+ xml: &UblFile,
+ ) -> Result {
+ let metadata = xml.metadata()?;
+
+ let filename_without_extension = filename_formatted_without_extension(
+ &metadata.document_type,
+ &metadata.document_id,
+ &metadata.ruc,
+ )
+ .ok_or(FileSenderError {
+ message: "Could not determine the filename of the file to be sent".to_string(),
+ client_error: None,
+ })?;
+
+ let send_target = send_file_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &self.urls,
+ )
+ .ok_or(FileSenderError {
+ message: "Could not determine the target for the file to be sent".to_string(),
+ client_error: None,
+ })?;
+ let verify_ticket_target = verify_ticket_target(
+ &metadata.document_type,
+ &metadata.voided_line_document_type_code,
+ &self.urls,
+ );
+
+ let zip = create_zip(&filename_without_extension, &xml.file_content.clone())?;
+ let zip_base64 = general_purpose::STANDARD.encode(zip);
+
+ let file_to_be_sent = File {
+ name: format!("{filename_without_extension}.zip"),
+ base_64: zip_base64,
+ };
+
+ let client = ClientSUNAT::default();
+ let result = client
+ .send_file(&send_target, &file_to_be_sent, &self.credentials)
+ .await?;
+
+ Ok(SendFileResponseWrapper {
+ data: result,
+ send_file_target: send_target,
+ verify_ticket_target,
+ })
+ }
+
+ pub async fn verify_ticket(
+ &self,
+ target: &VerifyTicketTarget,
+ ticket: &str,
+ ) -> Result {
+ let client = ClientSUNAT::default();
+ client
+ .verify_ticket(target, ticket, &self.credentials)
+ .await
+ }
+}
+
+impl From for FileSenderError {
+ fn from(value: UblMetadataError) -> Self {
+ Self {
+ message: value.message.to_string(),
+ client_error: None,
+ }
+ }
+}
+
+impl From for FileSenderError {
+ fn from(value: ZipError) -> Self {
+ Self {
+ message: value.to_string(),
+ client_error: None,
+ }
+ }
+}
+
+impl From for FileSenderError {
+ fn from(value: ErrorClientSUNAT) -> Self {
+ Self {
+ message: "Error while sending the file".to_string(),
+ client_error: Some(value),
+ }
+ }
+}
diff --git a/xsender/src/global.rs b/xsender/src/global.rs
new file mode 100644
index 00000000..372c3704
--- /dev/null
+++ b/xsender/src/global.rs
@@ -0,0 +1,24 @@
+use regex::Regex;
+use tera::Tera;
+
+pub const CBC_NS: &str = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2";
+pub const CAC_NS: &str = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
+pub const SAC_NS: &str =
+ "urn:sunat:names:specification:ubl:peru:schema:xsd:SunatAggregateComponents-1";
+
+lazy_static::lazy_static! {
+ pub static ref TEMPLATES: Tera = {
+ match Tera::new("src/templates/*.xml") {
+ Ok(t) => t,
+ Err(e) => {
+ println!("Parsing error(s): {}", e);
+ ::std::process::exit(1);
+ }
+ }
+ };
+
+ pub static ref FACTURA_SERIE_REGEX: Regex = Regex::new("^[F|f].*$").expect("Invalid FACTURA_SERIE_REGEX");
+ pub static ref BOLETA_SERIE_REGEX: Regex = Regex::new("^[B|f].*$").expect("Invalid BOLETA_SERIE_REGEX");
+ pub static ref GUIA_REMISION_REMITENTE_SERIE_REGEX: Regex = Regex::new("^[T|t].*$").expect("Invalid GUIA_REMISION_REMITENTE_SERIE_REGEX");
+ pub static ref GUIA_REMISION_TRANSPORTISTA_SERIE_REGEX: Regex = Regex::new("^[V|v].*$").expect("Invalid GUIA_REMISION_TRANSPORTISTA_SERIE_REGEX");
+}
diff --git a/xsender/src/lib.rs b/xsender/src/lib.rs
index b2a15b36..e21e4b11 100644
--- a/xsender/src/lib.rs
+++ b/xsender/src/lib.rs
@@ -1,4 +1,9 @@
-pub mod analyzer;
-pub mod message_renderer;
-pub mod metadata;
-pub mod xml;
+mod analyzer;
+mod client_sunat;
+mod file_sender;
+mod global;
+mod models;
+pub mod prelude;
+mod soap;
+mod ubl_file;
+mod zip_manager;
diff --git a/xsender/src/models.rs b/xsender/src/models.rs
new file mode 100644
index 00000000..ee80a5f8
--- /dev/null
+++ b/xsender/src/models.rs
@@ -0,0 +1,76 @@
+/// Set of URL where the company should send his documents
+#[derive(Clone)]
+pub struct Urls {
+ pub invoice: String,
+ pub perception_retention: String,
+ pub despatch: String,
+}
+
+/// Credentials of the company for sending documents
+#[derive(Clone)]
+pub struct Credentials {
+ pub username: String,
+ pub password: String,
+}
+
+/// Defines where the document should be sent
+#[derive(Debug, PartialEq)]
+pub enum SendFileTarget {
+ /// URL and Method
+ Soap(String, SoapFileTargetAction),
+
+ /// URL and Method
+ Rest(String, RestFileTargetAction),
+}
+
+/// Defines where the document should be sent
+#[derive(Debug, PartialEq)]
+pub enum VerifyTicketTarget {
+ /// URL and Method
+ Soap(String),
+
+ /// URL and Method
+ Rest(String),
+}
+
+#[derive(Debug, PartialEq)]
+pub enum SoapFileTargetAction {
+ /// Sends a single file
+ Bill,
+ /// Send summary-documents or voided-documents
+ Summary,
+ /// Sends a set of files all together
+ Pack,
+}
+
+#[derive(Debug, PartialEq)]
+pub enum RestFileTargetAction {
+ SendDocument,
+}
+
+pub struct DocumentType {}
+
+impl DocumentType {
+ pub const INVOICE: &'static str = "Invoice";
+ pub const CREDIT_NOTE: &'static str = "CreditNote";
+ pub const DEBIT_NOTE: &'static str = "DebitNote";
+ pub const VOIDED_DOCUMENTS: &'static str = "VoidedDocuments";
+ pub const SUMMARY_DOCUMENTS: &'static str = "SummaryDocuments";
+ pub const DESPATCH_ADVICE: &'static str = "DespatchAdvise";
+ pub const PERCEPTION: &'static str = "Perception";
+ pub const RETENTION: &'static str = "Retention";
+}
+
+pub struct Catalog1 {}
+
+#[allow(dead_code)]
+impl Catalog1 {
+ pub const FACTURA: &'static str = "01";
+ pub const BOLETA: &'static str = "03";
+ pub const NOTA_CREDITO: &'static str = "07";
+ pub const NOTA_DEBITO: &'static str = "08";
+ pub const GUIA_REMISION_REMITENTE: &'static str = "09";
+ pub const RETENCION: &'static str = "20";
+ pub const GUIA_REMISION_TRANSPORTISTA: &'static str = "31";
+ pub const PERCEPCION: &'static str = "40";
+}
diff --git a/xsender/src/prelude.rs b/xsender/src/prelude.rs
new file mode 100644
index 00000000..8290f5c5
--- /dev/null
+++ b/xsender/src/prelude.rs
@@ -0,0 +1,4 @@
+pub use crate::client_sunat::*;
+pub use crate::file_sender::*;
+pub use crate::models::*;
+pub use crate::ubl_file::*;
diff --git a/xsender/src/soap/cdr.rs b/xsender/src/soap/cdr.rs
new file mode 100644
index 00000000..ec047a03
--- /dev/null
+++ b/xsender/src/soap/cdr.rs
@@ -0,0 +1,158 @@
+use std::str::FromStr;
+
+use xml::name::OwnedName;
+use xml::reader::XmlEvent;
+use xml::EventReader;
+
+use crate::global::{CAC_NS, CBC_NS};
+
+pub struct Cdr {
+ pub response_code: String,
+ pub description: String,
+ pub notes: Vec,
+}
+
+#[derive(Debug)]
+pub struct CdrReadError {
+ pub message: String,
+}
+
+impl FromStr for Cdr {
+ type Err = CdrReadError;
+
+ fn from_str(s: &str) -> Result {
+ let event_reader = EventReader::from_str(s);
+
+ enum Wrapper {
+ Response,
+ Note,
+ }
+
+ let mut current_wrapper: Option = None;
+ let mut current_text: String = String::from("");
+
+ // Send bill data
+ let mut response_code: Option = None;
+ let mut description: Option = None;
+
+ // Send summary data
+ let mut notes: Vec = Vec::new();
+
+ fn set_wrapper(
+ is_start: bool,
+ name: &OwnedName,
+ mut current_wrapper: Option,
+ ) -> Option {
+ let namespace = name.namespace.as_deref();
+ let prefix = name.prefix.as_deref();
+ let local_name = name.local_name.as_str();
+
+ match (namespace, prefix, local_name) {
+ (Some(CAC_NS), Some("cac"), "Response") => {
+ current_wrapper = if is_start {
+ Some(Wrapper::Response)
+ } else {
+ None
+ }
+ }
+ (Some(CBC_NS), Some("cbc"), "Note") => {
+ current_wrapper = if is_start { Some(Wrapper::Note) } else { None }
+ }
+ _ => {}
+ };
+
+ current_wrapper
+ }
+
+ for event in event_reader {
+ match event {
+ Ok(XmlEvent::StartElement { name, .. }) => {
+ current_wrapper = set_wrapper(true, &name, current_wrapper);
+ }
+ Ok(XmlEvent::EndElement { name }) => {
+ let namespace = name.namespace.as_deref();
+ let prefix = name.prefix.as_deref();
+ let local_name = name.local_name.as_str();
+
+ match (¤t_wrapper, &namespace, &prefix, local_name) {
+ (Some(Wrapper::Response), Some(CBC_NS), Some("cbc"), "ResponseCode") => {
+ response_code = Some(current_text.clone());
+ }
+ (Some(Wrapper::Response), Some(CBC_NS), Some("cbc"), "Description") => {
+ description = Some(current_text.clone());
+ }
+ (Some(Wrapper::Note), Some(CBC_NS), Some("cbc"), "Note") => {
+ notes.push(current_text.clone());
+ }
+ _ => {}
+ };
+
+ current_wrapper = set_wrapper(false, &name, current_wrapper);
+ }
+ Ok(XmlEvent::Characters(characters)) => {
+ current_text = characters;
+ }
+ Err(_) => {
+ break;
+ }
+ _ => {}
+ }
+ }
+
+ match (response_code, description) {
+ (Some(response_code), Some(description)) => Ok(Self {
+ description,
+ response_code,
+ notes,
+ }),
+ _ => Err(CdrReadError {
+ message: "Could not find response_code and description inside the xml".to_string(),
+ }),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::fs;
+ use std::str::FromStr;
+
+ use crate::soap::cdr::Cdr;
+
+ const RESOURCES: &str = "resources/test";
+
+ #[test]
+ fn read_cdr_without_notes() {
+ let file_content =
+ fs::read_to_string(&format!("{RESOURCES}/R-12345678901-01-F001-00000587.xml"))
+ .expect("Could not read file");
+ let cdr = Cdr::from_str(&file_content).expect("Could not read Cdr");
+
+ assert_eq!(cdr.response_code, "0");
+ assert_eq!(
+ cdr.description,
+ "La Factura numero F001-00000587, ha sido aceptada"
+ );
+ assert!(cdr.notes.is_empty());
+ }
+
+ #[test]
+ fn read_cdr_with_notes() {
+ let file_content =
+ fs::read_to_string(&format!("{RESOURCES}/R-20220557805-01-F001-22Openubl.xml"))
+ .expect("Could not read file");
+ let cdr = Cdr::from_str(&file_content).expect("Could not read Cdr");
+
+ assert_eq!(cdr.response_code, "0");
+ assert_eq!(
+ cdr.description,
+ "La Factura numero F001-22, ha sido aceptada"
+ );
+ assert_eq!(cdr.notes.len(), 8);
+ assert!(cdr
+ .notes
+ .last()
+ .unwrap()
+ .starts_with("4252 - El dato ingresado como atributo @listName es incorrecto."));
+ }
+}
diff --git a/xsender/src/message_renderer.rs b/xsender/src/soap/envelope.rs
similarity index 72%
rename from xsender/src/message_renderer.rs
rename to xsender/src/soap/envelope.rs
index a8f792b7..85063b69 100644
--- a/xsender/src/message_renderer.rs
+++ b/xsender/src/soap/envelope.rs
@@ -1,13 +1,16 @@
use serde::Serialize;
-use tera::{Context, Tera};
+use tera::{Context, Result};
-pub struct SoapMessage {
+use crate::global::TEMPLATES;
+
+pub struct EnvelopeData {
pub username: String,
pub password: String,
- pub body: SoapBody,
+ pub body: BodyData,
}
-pub enum SoapBody {
+#[allow(dead_code)]
+pub enum BodyData {
SendBill(FileData),
SendSummary(FileData),
VerifyTicket(String),
@@ -42,50 +45,38 @@ pub struct DocumentCdpData {
importe_total: String,
}
-pub trait RendererSoap {
- fn render(&self) -> tera::Result;
+pub trait ToStringXml {
+ fn to_string_xml(&self) -> Result;
}
-lazy_static::lazy_static! {
- pub static ref TEMPLATES: Tera = {
- match Tera::new("src/templates/*.xml") {
- Ok(t) => t,
- Err(e) => {
- println!("Parsing error(s): {}", e);
- ::std::process::exit(1);
- }
- }
- };
-}
-
-impl RendererSoap for SoapMessage {
- fn render(&self) -> tera::Result {
+impl ToStringXml for EnvelopeData {
+ fn to_string_xml(&self) -> Result {
let mut context = Context::new();
context.insert("username", &self.username);
context.insert("password", &self.password);
match &self.body {
- SoapBody::SendBill(file_data) => {
+ BodyData::SendBill(file_data) => {
context.insert("body", &file_data);
TEMPLATES.render("send_bill.xml", &context)
}
- SoapBody::SendSummary(file_data) => {
+ BodyData::SendSummary(file_data) => {
context.insert("body", &file_data);
TEMPLATES.render("send_summary.xml", &context)
}
- SoapBody::VerifyTicket(ticket) => {
+ BodyData::VerifyTicket(ticket) => {
context.insert("ticket", ticket);
TEMPLATES.render("verify_ticket.xml", &context)
}
- SoapBody::GetStatusCrd(document_data) => {
+ BodyData::GetStatusCrd(document_data) => {
context.insert("body", document_data);
TEMPLATES.render("get_status_crd.xml", &context)
}
- SoapBody::ValidateCdpCriterios(document_data) => {
+ BodyData::ValidateCdpCriterios(document_data) => {
context.insert("body", document_data);
TEMPLATES.render("validate_cdp_criterios.xml", &context)
}
- SoapBody::ValidateFile(file_data) => {
+ BodyData::ValidateFile(file_data) => {
context.insert("body", &file_data);
TEMPLATES.render("validate_file.xml", &context)
}
@@ -95,16 +86,14 @@ impl RendererSoap for SoapMessage {
#[cfg(test)]
mod tests {
- use crate::message_renderer::SoapBody::{
+ use crate::soap::envelope::BodyData::{
GetStatusCrd, SendBill, SendSummary, ValidateCdpCriterios, ValidateFile, VerifyTicket,
};
- use crate::message_renderer::{
- DocumentCdpData, DocumentData, FileData, RendererSoap, SoapMessage,
- };
+ use crate::soap::envelope::*;
#[test]
fn send_bill() {
- let message = SoapMessage {
+ let message = EnvelopeData {
username: String::from("my_username"),
password: String::from("my_password"),
body: SendBill(FileData {
@@ -113,13 +102,12 @@ mod tests {
}),
};
- let result = message.render();
- assert!(result.is_ok());
+ assert!(message.to_string_xml().unwrap().len() > 1);
}
#[test]
fn send_summary() {
- let message = SoapMessage {
+ let message = EnvelopeData {
username: String::from("my_username"),
password: String::from("my_password"),
body: SendSummary(FileData {
@@ -128,25 +116,23 @@ mod tests {
}),
};
- let result = message.render();
- assert!(result.is_ok());
+ assert!(message.to_string_xml().unwrap().len() > 1);
}
#[test]
fn verify_ticket() {
- let message = SoapMessage {
+ let message = EnvelopeData {
username: String::from("my_username"),
password: String::from("my_password"),
body: VerifyTicket(String::from("my_ticket")),
};
- let result = message.render();
- assert!(result.is_ok());
+ assert!(message.to_string_xml().unwrap().len() > 1);
}
#[test]
fn get_status_crd() {
- let message = SoapMessage {
+ let message = EnvelopeData {
username: String::from("my_username"),
password: String::from("my_password"),
body: GetStatusCrd(DocumentData {
@@ -157,13 +143,12 @@ mod tests {
}),
};
- let result = message.render();
- assert!(result.is_ok());
+ assert!(message.to_string_xml().unwrap().len() > 1);
}
#[test]
fn validate_cdp_criterios() {
- let message = SoapMessage {
+ let message = EnvelopeData {
username: String::from("my_username"),
password: String::from("my_password"),
body: ValidateCdpCriterios(DocumentCdpData {
@@ -178,13 +163,12 @@ mod tests {
}),
};
- let result = message.render();
- assert!(result.is_ok());
+ assert!(message.to_string_xml().unwrap().len() > 1);
}
#[test]
fn validate_file() {
- let message = SoapMessage {
+ let message = EnvelopeData {
username: String::from("my_username"),
password: String::from("my_password"),
body: ValidateFile(FileData {
@@ -193,7 +177,6 @@ mod tests {
}),
};
- let result = message.render();
- assert!(result.is_ok());
+ assert!(message.to_string_xml().unwrap().len() > 1);
}
}
diff --git a/xsender/src/soap/mod.rs b/xsender/src/soap/mod.rs
new file mode 100644
index 00000000..3cad0ad5
--- /dev/null
+++ b/xsender/src/soap/mod.rs
@@ -0,0 +1,4 @@
+pub mod cdr;
+pub mod envelope;
+pub mod send_file_response;
+pub mod verify_ticket_response;
diff --git a/xsender/src/soap/send_file_response.rs b/xsender/src/soap/send_file_response.rs
new file mode 100644
index 00000000..f512b615
--- /dev/null
+++ b/xsender/src/soap/send_file_response.rs
@@ -0,0 +1,185 @@
+use std::str::FromStr;
+
+use xml::name::OwnedName;
+use xml::reader::XmlEvent;
+use xml::EventReader;
+
+pub enum SendFileXmlResponse {
+ /// String contains the base64 CDR zip file
+ Cdr(String),
+
+ /// String contains the ticket code
+ Ticket(String),
+
+ /// (Code, Message)
+ Fault(String, String),
+}
+
+#[derive(Debug)]
+pub struct SendFileXmlResponseFromStrError {}
+
+impl FromStr for SendFileXmlResponse {
+ type Err = SendFileXmlResponseFromStrError;
+
+ fn from_str(s: &str) -> Result {
+ let event_reader = EventReader::from_str(s);
+
+ enum Wrapper {
+ SendBillResponse,
+ SendSummaryResponse,
+ Fault,
+ }
+
+ let mut current_wrapper: Option = None;
+ let mut current_text: String = String::from("");
+
+ // Send bill data
+ let mut application_response: Option = None;
+
+ // Send summary data
+ let mut ticket: Option = None;
+
+ // Fault data
+ let mut fault_code: Option = None;
+ let mut fault_message: Option = None;
+
+ fn set_wrapper(
+ is_start: bool,
+ name: &OwnedName,
+ mut current_wrapper: Option,
+ ) -> Option {
+ let prefix = name.prefix.as_deref();
+ let local_name = name.local_name.as_str();
+
+ match (prefix, local_name) {
+ (Some("br"), "sendBillResponse") => {
+ current_wrapper = if is_start {
+ Some(Wrapper::SendBillResponse)
+ } else {
+ None
+ }
+ }
+ (Some("br"), "sendSummaryResponse") => {
+ current_wrapper = if is_start {
+ Some(Wrapper::SendSummaryResponse)
+ } else {
+ None
+ }
+ }
+ (Some("soap-env"), "Fault") => {
+ current_wrapper = if is_start { Some(Wrapper::Fault) } else { None }
+ }
+ _ => {}
+ };
+
+ current_wrapper
+ }
+
+ for event in event_reader {
+ match event {
+ Ok(XmlEvent::StartElement { name, .. }) => {
+ current_wrapper = set_wrapper(true, &name, current_wrapper);
+ }
+ Ok(XmlEvent::EndElement { name }) => {
+ let local_name = name.local_name.as_str();
+
+ match (¤t_wrapper, local_name) {
+ (Some(Wrapper::SendBillResponse), "applicationResponse") => {
+ application_response = Some(current_text.clone());
+ }
+ (Some(Wrapper::SendSummaryResponse), "ticket") => {
+ ticket = Some(current_text.clone());
+ }
+ (Some(Wrapper::Fault), "faultcode") => {
+ fault_code = Some(current_text.clone());
+ }
+ (Some(Wrapper::Fault), "faultstring") => {
+ fault_message = Some(current_text.clone());
+ }
+ _ => {}
+ };
+
+ current_wrapper = set_wrapper(false, &name, current_wrapper);
+ }
+ Ok(XmlEvent::Characters(characters)) => {
+ current_text = characters;
+ }
+ Err(_) => {
+ break;
+ }
+ _ => {}
+ }
+ }
+
+ match (application_response, ticket, fault_code, fault_message) {
+ (Some(application_response), _, _, _) => Ok(Self::Cdr(application_response)),
+ (_, Some(ticket), _, _) => Ok(Self::Ticket(ticket)),
+ (_, _, Some(fault_code), Some(fault_message)) => {
+ Ok(Self::Fault(fault_code, fault_message))
+ }
+ _ => Err(SendFileXmlResponseFromStrError {}),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::fs;
+ use std::str::FromStr;
+
+ use crate::soap::send_file_response::SendFileXmlResponse;
+
+ const RESOURCES: &str = "resources/test";
+
+ #[test]
+ fn read_ok_response() {
+ let file_content = fs::read_to_string(&format!("{RESOURCES}/bill_service_response_ok.xml"))
+ .expect("Could not read file");
+ let response =
+ SendFileXmlResponse::from_str(&file_content).expect("Could not read response");
+
+ let cdr = match response {
+ SendFileXmlResponse::Cdr(cdr_base64) => cdr_base64,
+ SendFileXmlResponse::Ticket(_) => "".to_string(),
+ SendFileXmlResponse::Fault(_, _) => "".to_string(),
+ };
+
+ let expected_cdr = "UEsDBBQAAgAIAEwklVcAAAAAAgAAAAAAAAAGAAAAZHVtbXkvAwBQSwMEFAACAAgATCSVVwwGkuIxBAAAGQ0AABsAAABSLTEyMzQ1Njc4OTEyLTAxLUYwMDEtMS54bWy1V2FP2zwQ/r5fEZUPk6Y3OElbWKPQqVDGsgFitDC0byY52uhN7WA7peXXv+e4SdMStHbSq/LBuXvuubvHZ1sEXxaz1JqDkAlnJy330GlZwCIeJ2xy0robf7U/t770PwRU+IMsS5OIKgTegsw4k2BhMJMnrVwwn1OZSJ/RGUhfZhAlTyuwnz+mvoymMKP+QsZ+yOY8icD2Wibcp2JPhoZK1mywUHvSnfHZjLPzhQKmVcBPpASm5Jo0eoz+ivQU4VEjIf07wsFkImBCFTSRxrgVU6Uyn5CXl5fDl/YhFxPiOY5DnB5BTCyTyUGJlpxmFd4kkofo0vYiUC8IsDmkPANSJcHkVRgsZKoKsDZLm7LYVgn2UiUp+5Q5o+rdPjMQeb3ZkUY39eqWxIv3enXJw9XlqKAqscgCi6yhaHTkKRU2egVIvfmy1Q9wgvy708tqIGQ55g0+Y6nNDsOV6gejZIId5KI6IjvsCx4zHQZxyJ54/4NlBWeUcYY6pclrodUVqCmPrUE64SJR09m7EriOpsW+IjtyO+zgF6L1AGkNW6TgrircmdTplLXaMy7gQEhqyyntut6K8haeQODtAdbdbajlQiOax4Iy+cTFTBpD3fTHtBsSlcMY27Ks3qTek3QXgZCQbFceDJMJSLWnYqjIQV2niueepjn0H16PRPsBEvXt+ntGw3m4hMvrQXZ+OoB0zKYv98+/w2V4/hS/dm/uur+PB/C8OLvoEX41ypxzGf5KLn9cPF9E38/vczn8OVVikRz9PDkJSD2L3h9SbRCOGtmctfpEmIhPNyKZ4+mz/oWl9fEUFL3Bo4rXGQj10WJcWXn2ydDUooIfsCw4g4eu0xtSRc1KR5kzj8zXeA3EVrQ2rfhNQmSo8W8HF2yhlDmIEYiEpnWLJt6fvhZbcBne63z2CGJ/to3oeoKyXLJWhlRqrXXEdfOdQt5ePm9Msh/gW6VN9+ZND4d979AJyBtrgTvLpeKz1e2CRreEbjsKtAYcO20XJ/mo2z72DLTy6iaHeos8x+3Zrmd7nbHj+MXfClpB1hFjfC76DbDCXsDKN37F7bULbtdgN5wbcEPc8dsd3+tuglfcNPJrqq960ZbR3fVgXOuuAnKxvKFCLY2tWIYxbk71mlU0qEEbf16v210TkfejSoeZQh1QrGqVGA/ZQpL3isPDnyiaVg0OlKLRdFZMkvbrkRGMpus7wUzObdg/2NJA20yihiDyp2RkW2f9CSwG8f9ISRoT3EIEyXznnK7X7nSPjj/3XG/nnA0phjzKtQrl4JW1VF/FUK60xBRf8dWwq8le2zcG+4zHONibE13YCtQQZCSSrKjrklpfaYSyU4thHYJbJsE/1pRaMom5RSPIFI2pYavHlj3VC1+3szEzW4VXUjXBjU5JlqB9x704wvNe/fbZjY0spHk/SPN/Nv3/AFBLAQIAABQAAgAIAEwklVcAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAABkdW1teS9QSwECAAAUAAIACABMJJVXDAaS4jEEAAAZDQAAGwAAAAAAAAABAAAAAAAmAAAAUi0xMjM0NTY3ODkxMi0wMS1GMDAxLTEueG1sUEsFBgAAAAACAAIAfQAAAJAEAAAAAA==";
+ assert_eq!(expected_cdr, cdr);
+ }
+
+ #[test]
+ fn read_fault_response() {
+ let file_content =
+ fs::read_to_string(&format!("{RESOURCES}/bill_service_response_fault.xml"))
+ .expect("Could not read file");
+ let response =
+ SendFileXmlResponse::from_str(&file_content).expect("Could not read response");
+
+ let fault = match response {
+ SendFileXmlResponse::Cdr(_) => ("".to_string(), "".to_string()),
+ SendFileXmlResponse::Ticket(_) => ("".to_string(), "".to_string()),
+ SendFileXmlResponse::Fault(error, description) => (error, description),
+ };
+
+ assert_eq!("soap-env:Client.0111", fault.0);
+ assert_eq!("No tiene el perfil para enviar comprobantes electronicos - Detalle: Rejected by policy.", fault.1);
+ }
+
+ #[test]
+ fn read_ticket_response() {
+ let file_content =
+ fs::read_to_string(&format!("{RESOURCES}/bill_service_response_ticket.xml"))
+ .expect("Could not read file");
+ let response =
+ SendFileXmlResponse::from_str(&file_content).expect("Could not read response");
+
+ let ticket = match response {
+ SendFileXmlResponse::Cdr(_) => "".to_string(),
+ SendFileXmlResponse::Ticket(ticket) => ticket,
+ SendFileXmlResponse::Fault(_, _) => "".to_string(),
+ };
+
+ assert_eq!("1703154974517", ticket);
+ }
+}
diff --git a/xsender/src/soap/verify_ticket_response.rs b/xsender/src/soap/verify_ticket_response.rs
new file mode 100644
index 00000000..5c666432
--- /dev/null
+++ b/xsender/src/soap/verify_ticket_response.rs
@@ -0,0 +1,136 @@
+use std::str::FromStr;
+
+use xml::name::OwnedName;
+use xml::reader::XmlEvent;
+use xml::EventReader;
+
+pub enum VerifyTicketXmlResponse {
+ /// String contains the base64 CDR zip file
+ Cdr(String, String),
+
+ /// (Code, Message)
+ Fault(String, String),
+}
+
+#[derive(Debug)]
+pub struct VerifyTicketXmlResponseFromStrError {}
+
+impl FromStr for VerifyTicketXmlResponse {
+ type Err = VerifyTicketXmlResponseFromStrError;
+
+ fn from_str(s: &str) -> Result {
+ let event_reader = EventReader::from_str(s);
+
+ enum Wrapper {
+ StatusResponse,
+ Fault,
+ }
+
+ let mut current_wrapper: Option = None;
+ let mut current_text: String = String::from("");
+
+ // Send bill data
+ let mut content: Option = None;
+ let mut status_code: Option = None;
+
+ // Fault data
+ let mut fault_code: Option = None;
+ let mut fault_message: Option = None;
+
+ fn set_wrapper(
+ is_start: bool,
+ name: &OwnedName,
+ mut current_wrapper: Option,
+ ) -> Option {
+ let prefix = name.prefix.as_deref();
+ let local_name = name.local_name.as_str();
+
+ match (prefix, local_name) {
+ (Some("ns2"), "getStatusResponse") => {
+ current_wrapper = if is_start {
+ Some(Wrapper::StatusResponse)
+ } else {
+ None
+ }
+ }
+ (Some("soap-env"), "Fault") => {
+ current_wrapper = if is_start { Some(Wrapper::Fault) } else { None }
+ }
+ _ => {}
+ };
+
+ current_wrapper
+ }
+
+ for event in event_reader {
+ match event {
+ Ok(XmlEvent::StartElement { name, .. }) => {
+ current_wrapper = set_wrapper(true, &name, current_wrapper);
+ }
+ Ok(XmlEvent::EndElement { name }) => {
+ let local_name = name.local_name.as_str();
+
+ match (¤t_wrapper, local_name) {
+ (Some(Wrapper::StatusResponse), "content") => {
+ content = Some(current_text.clone());
+ }
+ (Some(Wrapper::StatusResponse), "statusCode") => {
+ status_code = Some(current_text.clone());
+ }
+ (Some(Wrapper::Fault), "faultcode") => {
+ fault_code = Some(current_text.clone());
+ }
+ (Some(Wrapper::Fault), "faultstring") => {
+ fault_message = Some(current_text.clone());
+ }
+ _ => {}
+ };
+
+ current_wrapper = set_wrapper(false, &name, current_wrapper);
+ }
+ Ok(XmlEvent::Characters(characters)) => {
+ current_text = characters;
+ }
+ Err(_) => {
+ break;
+ }
+ _ => {}
+ }
+ }
+
+ match (content, status_code, fault_code, fault_message) {
+ (Some(content), Some(status_code), _, _) => Ok(Self::Cdr(content, status_code)),
+ (_, _, Some(fault_code), Some(fault_message)) => {
+ Ok(Self::Fault(fault_code, fault_message))
+ }
+ _ => Err(VerifyTicketXmlResponseFromStrError {}),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::fs;
+ use std::str::FromStr;
+
+ use crate::soap::verify_ticket_response::VerifyTicketXmlResponse;
+
+ const RESOURCES: &str = "resources/test";
+
+ #[test]
+ fn read_ok_response() {
+ let file_content = fs::read_to_string(&format!("{RESOURCES}/get_status_response_ok.xml"))
+ .expect("Could not read file");
+ let response =
+ VerifyTicketXmlResponse::from_str(&file_content).expect("Could not read response");
+
+ let result = match response {
+ VerifyTicketXmlResponse::Cdr(cdr_base64, status_code) => (cdr_base64, status_code),
+ VerifyTicketXmlResponse::Fault(_, _) => ("".to_string(), "".to_string()),
+ };
+
+ let expected_cdr = "UEsDBBQAAgAIAK07lVcAAAAAAgAAAAAAAAAGAAAAZHVtbXkvAwBQSwMEFAACAAgArTuVV2PumuIyBAAACA0AAB8AAABSLTEyMzQ1Njc4OTEyLVJBLTIwMjAwMzI4LTEueG1stVdRT+M4EH7fXxGVh5X2LjhJoYUoZFUo3FULPba0gPbNJEMbLrFzttMWfv2OkyZNS9C2K53gwZn55puZz2MbvK/LJDbmIGTE2VnLPrRaBrCAhxGbnrUm4yvzpPXV/+RR4fbSNI4CqhA4AplyJsHAYCbPWplgLqcyki6jCUhXphBEzyuwmz3FrgxmkFB3KUN3wOY8CsB0WkW4S8WeDA2VrNlgqfaku+BJwtnlUgHTKuAnUgJTck0aPAW/RXqO8KCRkP4eYW86FTClCppIQ9yKmVKpS8hisThctA+5mBLHsixinRLEhDKaHpRoyWla4YtE8hBd2p4H6gUBNoeYp0CqJJi8CoOljFUO1mZpUhaaKsJeqiRlnzJjVH3YZwoiqzd7p9FNvdol8fKjXm3yeHN9l1OVWGSBZdpQNDqymAoTvQKk3nzZ8j2cIHdyfl0NhCzHvMFXWGqzw3ClfO8ummIHmaiOyA77gsdMh0E4YM/c/2QY3gVlnKFOcfSWa3UDasZDoxdPuYjULPlQAtvStNhXYAb2ETt4QLQeIK1hi+TcVYU7k1pHZa1mwgUcCElNOaPHtrOiHMEzCLw9wJiMBlouNKJ5LCiTz1wksjDUTb9MuyFROYyhKcvqi9R7ku4iEBKS7cq9fjQFqfZUDBU5qOtU8dzTOAP/hS7D/iWx5peB9eMvMumyf9KnvmN/z0bf528L/kYerkZXr7Ph+ViSm5voPusOX5aTq6Pn3n8PCfs7kT+GycPL203YW9BR6jyO/7gNF2dnHqln0ftDqg3CUSObs1afiCLiy62I5nj6jH/h1fh8Dore4lHF6wyE+mwwrows/VLQ1KK8b/Cac3qPx9ZpnyparHRUceaReYjXQGgEa9OKv0iIDDX+7eCcbSBlBuIORETjukUT709fi825Ct5hljyB2J9tI7qeoCyXrJUhlVprHXHdfKeQ95fPO5P0PXyrtOm+eNMHfd85tDzyzprjLjKpeLK6XdBol9BtR47WgK7Vtjt2t9M5alt2ga3cukvsC0jte4yvgW9Zbv67wlf2HFY+4TrSdyynbdqO6ay4N5wb8IK46zqnrtPZBK+4aeDWRF1Vqi13k2FvXKu9AnLxekuFei1s+XIQovbVY1XROJbdxh/n9Ph4TUQ+jiodxZDpgHxVq6TwkC0k+ag4PNuRonHVYE8pGsySfFC0X0+EYDReH/liMEYD/2BLA20rEjUEkV8lI9s6609gIYj/R0rSmGAEAUTznXPaTvvouNM9ObWdnXM2pOjzINMqlINX1lJ95UO50hJTjHomDrhltZ0TsxrwtXtjvi94iPO9Odi5LUf1QQYiSvPyrqmBT1em/1AI8NsIwXiiL9TYyPanMaOGjEJu0ABSRUNaUNeJyj7rzaxb3Jij5mYqFZuiCgmjNEL7jtvUMffZnQ1q0rw/pPkfGf8nUEsBAgAAFAACAAgArTuVVwAAAAACAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAGR1bW15L1BLAQIAABQAAgAIAK07lVdj7priMgQAAAgNAAAfAAAAAAAAAAEAAAAAACYAAABSLTEyMzQ1Njc4OTEyLVJBLTIwMjAwMzI4LTEueG1sUEsFBgAAAAACAAIAgQAAAJUEAAAAAA==";
+ assert_eq!(expected_cdr, result.0);
+ assert_eq!("0", result.1);
+ }
+}
diff --git a/xsender/src/metadata.rs b/xsender/src/ubl_file.rs
similarity index 74%
rename from xsender/src/metadata.rs
rename to xsender/src/ubl_file.rs
index 594447b6..e51a4e41 100644
--- a/xsender/src/metadata.rs
+++ b/xsender/src/ubl_file.rs
@@ -1,26 +1,41 @@
+use std::fs;
+use std::path::Path;
+
use xml::reader::XmlEvent;
use xml::EventReader;
-use crate::xml::Xml;
+use crate::global::{CAC_NS, CBC_NS, SAC_NS};
-const CBC_NS: &str = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2";
-const CAC_NS: &str = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
-const SAC_NS: &str = "urn:sunat:names:specification:ubl:peru:schema:xsd:SunatAggregateComponents-1";
+pub struct UblFile {
+ pub file_content: String,
+}
+
+pub trait FromPath {
+ fn from_path(path: &Path) -> std::io::Result;
+}
+
+impl FromPath for UblFile {
+ fn from_path(path: &Path) -> std::io::Result {
+ let file_content = fs::read_to_string(path)?;
+ Ok(UblFile { file_content })
+ }
+}
#[derive(Debug)]
-pub struct Metadata {
+pub struct UblMetadata {
pub document_type: String,
- pub document_id: Option,
- pub ruc: Option,
+ pub document_id: String,
+ pub ruc: String,
pub voided_line_document_type_code: Option,
}
-pub trait MetadataProvider {
- fn get_metadata(&self) -> Result;
+#[derive(Debug)]
+pub struct UblMetadataError {
+ pub message: String,
}
-impl MetadataProvider for Xml {
- fn get_metadata(&self) -> Result {
+impl UblFile {
+ pub fn metadata(&self) -> Result {
let event_reader = EventReader::from_str(&self.file_content);
enum Wrapper {
@@ -104,14 +119,16 @@ impl MetadataProvider for Xml {
}
}
- match document_type {
- Some(document_type) => Ok(Metadata {
+ match (document_type, document_id, ruc) {
+ (Some(document_type), Some(document_id), Some(ruc)) => Ok(UblMetadata {
document_type,
document_id,
ruc,
voided_line_document_type_code,
}),
- None => Err("document_type could not be identified"),
+ _ => Err(UblMetadataError {
+ message: "document_type, document_id, and ruc were not found".to_string(),
+ }),
}
}
}
@@ -120,25 +137,24 @@ impl MetadataProvider for Xml {
mod tests {
use std::path::Path;
- use crate::metadata::MetadataProvider;
- use crate::xml::{FromPath, Xml};
+ use crate::ubl_file::{FromPath, UblFile};
const RESOURCES: &str = "resources/test";
#[test]
fn metadata() {
- let file1 = Xml::from_path(Path::new(&format!("{RESOURCES}/F001-1.xml")));
- let metadata1 = file1.unwrap().get_metadata().unwrap();
+ let file1 = UblFile::from_path(Path::new(&format!("{RESOURCES}/F001-1.xml")));
+ let metadata1 = file1.unwrap().metadata().unwrap();
assert_eq!(metadata1.document_type, "Invoice");
- assert_eq!(metadata1.document_id.as_deref(), Some("F001-1"));
- assert_eq!(metadata1.ruc.as_deref(), Some("12345678912"));
+ assert_eq!(metadata1.document_id, "F001-1");
+ assert_eq!(metadata1.ruc, "12345678912");
assert_eq!(metadata1.voided_line_document_type_code, None);
- let file2 = Xml::from_path(Path::new(&format!("{RESOURCES}/RA-20200328-1.xml")));
- let metadata2 = file2.unwrap().get_metadata().unwrap();
+ let file2 = UblFile::from_path(Path::new(&format!("{RESOURCES}/RA-20200328-1.xml")));
+ let metadata2 = file2.unwrap().metadata().unwrap();
assert_eq!(metadata2.document_type, "VoidedDocuments");
- assert_eq!(metadata2.document_id.as_deref(), Some("RA-20200328-1"));
- assert_eq!(metadata2.ruc.as_deref(), Some("12345678912"));
+ assert_eq!(metadata2.document_id, "RA-20200328-1");
+ assert_eq!(metadata2.ruc, "12345678912");
assert_eq!(
metadata2.voided_line_document_type_code.as_deref(),
Some("01")
diff --git a/xsender/src/xml.rs b/xsender/src/xml.rs
deleted file mode 100644
index 134238ed..00000000
--- a/xsender/src/xml.rs
+++ /dev/null
@@ -1,39 +0,0 @@
-use std::fs;
-use std::path::Path;
-
-pub struct Xml {
- pub filename: Option,
- pub file_content: String,
-}
-
-pub trait FromPath {
- fn from_path(path: &Path) -> std::io::Result;
-}
-
-impl FromPath for Xml {
- fn from_path(path: &Path) -> std::io::Result {
- let filename = path
- .file_name()
- .and_then(|v| v.to_str())
- .map(|v| v.to_string());
- let file_content = fs::read_to_string(path)?;
- Ok(Xml {
- filename,
- file_content,
- })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::path::Path;
-
- use crate::xml::{FromPath, Xml};
-
- #[test]
- fn from_file() {
- let xml_file = Xml::from_path(Path::new("resources/test/F001-1.xml"));
-
- assert_eq!(xml_file.unwrap().filename.unwrap(), "F001-1.xml");
- }
-}
diff --git a/xsender/src/zip_manager.rs b/xsender/src/zip_manager.rs
new file mode 100644
index 00000000..eb8a0266
--- /dev/null
+++ b/xsender/src/zip_manager.rs
@@ -0,0 +1,82 @@
+use std::io::{Cursor, Read, Write};
+
+use base64::engine::general_purpose;
+use base64::{DecodeError, Engine};
+use zip::result::{ZipError, ZipResult};
+use zip::write::FileOptions;
+use zip::{ZipArchive, ZipWriter};
+
+pub fn create_zip(filename_without_extension: &str, content: &str) -> ZipResult> {
+ let mut data = Vec::new();
+
+ {
+ let buff = Cursor::new(&mut data);
+ let mut zip = ZipWriter::new(buff);
+
+ let file_options = FileOptions::default();
+ zip.start_file(format!("{filename_without_extension}.xml"), file_options)?;
+ zip.write_all(content.as_bytes())?;
+ zip.finish()?;
+ }
+
+ Ok(data)
+}
+
+#[derive(Debug)]
+pub struct ExtractCdrFromBase64ZipError {
+ pub message: String,
+}
+
+pub fn extract_cdr_from_base64_zip(base64: &str) -> Result {
+ let zip_buf = general_purpose::STANDARD.decode(base64)?;
+ let reader = Cursor::new(zip_buf);
+ let mut archive = ZipArchive::new(reader)?;
+
+ let mut cdr_xml_content = "".to_string();
+ for index in 0..archive.len() {
+ let mut entry = archive.by_index(index)?;
+ if entry.is_file() && entry.name().ends_with(".xml") {
+ entry.read_to_string(&mut cdr_xml_content)?;
+ break;
+ }
+ }
+
+ Ok(cdr_xml_content)
+}
+
+impl From for ExtractCdrFromBase64ZipError {
+ fn from(e: DecodeError) -> Self {
+ ExtractCdrFromBase64ZipError {
+ message: e.to_string(),
+ }
+ }
+}
+
+impl From for ExtractCdrFromBase64ZipError {
+ fn from(e: ZipError) -> Self {
+ ExtractCdrFromBase64ZipError {
+ message: e.to_string(),
+ }
+ }
+}
+
+impl From for ExtractCdrFromBase64ZipError {
+ fn from(e: std::io::Error) -> Self {
+ ExtractCdrFromBase64ZipError {
+ message: e.to_string(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::zip_manager::extract_cdr_from_base64_zip;
+
+ #[test]
+ fn read_base64_zip() {
+ let cdr = "UEsDBBQAAgAIAEwklVcAAAAAAgAAAAAAAAAGAAAAZHVtbXkvAwBQSwMEFAACAAgATCSVVwwGkuIxBAAAGQ0AABsAAABSLTEyMzQ1Njc4OTEyLTAxLUYwMDEtMS54bWy1V2FP2zwQ/r5fEZUPk6Y3OElbWKPQqVDGsgFitDC0byY52uhN7WA7peXXv+e4SdMStHbSq/LBuXvuubvHZ1sEXxaz1JqDkAlnJy330GlZwCIeJ2xy0robf7U/t770PwRU+IMsS5OIKgTegsw4k2BhMJMnrVwwn1OZSJ/RGUhfZhAlTyuwnz+mvoymMKP+QsZ+yOY8icD2Wibcp2JPhoZK1mywUHvSnfHZjLPzhQKmVcBPpASm5Jo0eoz+ivQU4VEjIf07wsFkImBCFTSRxrgVU6Uyn5CXl5fDl/YhFxPiOY5DnB5BTCyTyUGJlpxmFd4kkofo0vYiUC8IsDmkPANSJcHkVRgsZKoKsDZLm7LYVgn2UiUp+5Q5o+rdPjMQeb3ZkUY39eqWxIv3enXJw9XlqKAqscgCi6yhaHTkKRU2egVIvfmy1Q9wgvy708tqIGQ55g0+Y6nNDsOV6gejZIId5KI6IjvsCx4zHQZxyJ54/4NlBWeUcYY6pclrodUVqCmPrUE64SJR09m7EriOpsW+IjtyO+zgF6L1AGkNW6TgrircmdTplLXaMy7gQEhqyyntut6K8haeQODtAdbdbajlQiOax4Iy+cTFTBpD3fTHtBsSlcMY27Ks3qTek3QXgZCQbFceDJMJSLWnYqjIQV2niueepjn0H16PRPsBEvXt+ntGw3m4hMvrQXZ+OoB0zKYv98+/w2V4/hS/dm/uur+PB/C8OLvoEX41ypxzGf5KLn9cPF9E38/vczn8OVVikRz9PDkJSD2L3h9SbRCOGtmctfpEmIhPNyKZ4+mz/oWl9fEUFL3Bo4rXGQj10WJcWXn2ydDUooIfsCw4g4eu0xtSRc1KR5kzj8zXeA3EVrQ2rfhNQmSo8W8HF2yhlDmIEYiEpnWLJt6fvhZbcBne63z2CGJ/to3oeoKyXLJWhlRqrXXEdfOdQt5ePm9Msh/gW6VN9+ZND4d979AJyBtrgTvLpeKz1e2CRreEbjsKtAYcO20XJ/mo2z72DLTy6iaHeos8x+3Zrmd7nbHj+MXfClpB1hFjfC76DbDCXsDKN37F7bULbtdgN5wbcEPc8dsd3+tuglfcNPJrqq960ZbR3fVgXOuuAnKxvKFCLY2tWIYxbk71mlU0qEEbf16v210TkfejSoeZQh1QrGqVGA/ZQpL3isPDnyiaVg0OlKLRdFZMkvbrkRGMpus7wUzObdg/2NJA20yihiDyp2RkW2f9CSwG8f9ISRoT3EIEyXznnK7X7nSPjj/3XG/nnA0phjzKtQrl4JW1VF/FUK60xBRf8dWwq8le2zcG+4zHONibE13YCtQQZCSSrKjrklpfaYSyU4thHYJbJsE/1pRaMom5RSPIFI2pYavHlj3VC1+3szEzW4VXUjXBjU5JlqB9x704wvNe/fbZjY0spHk/SPN/Nv3/AFBLAQIAABQAAgAIAEwklVcAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAABkdW1teS9QSwECAAAUAAIACABMJJVXDAaS4jEEAAAZDQAAGwAAAAAAAAABAAAAAAAmAAAAUi0xMjM0NTY3ODkxMi0wMS1GMDAxLTEueG1sUEsFBgAAAAACAAIAfQAAAJAEAAAAAA==";
+
+ let result = extract_cdr_from_base64_zip(cdr).expect("Could not extract cdr file");
+ assert!(!result.is_empty());
+ }
+}
diff --git a/xsender/tests/resources/e2e/12345678912-01-F001-1.xml b/xsender/tests/resources/e2e/12345678912-01-F001-1.xml
new file mode 100644
index 00000000..06818be9
--- /dev/null
+++ b/xsender/tests/resources/e2e/12345678912-01-F001-1.xml
@@ -0,0 +1,171 @@
+
+
+
+ dUdjbsfAegqmI/hBripxnCim3XM=Zro7GhJO4GASJgl7urUVy/mAOtfqO4lZMrHbdyFHPpk5ww7B/1fA85ykp/w0rvLP4hFrXPIqVkDf
+Z/aDjfWnOyvlNGR0Gha/wNGrompWEdPFcAp3Wuz/LPdhfSYhx0SpouG+S3HxIvL82FiL8XOjZQbz
+2N2h3/IFc6Ya6wlFNaeJdmaSxHgPIvTxUzuFjNCVorC2SCwnG+G7K5n1RLwwS/QHH8BJGaICsLH4
+faEqfdmXNMBJPf4u0Ya2EHPEi5EDzHZIkwWj11icptQIPa0mb6Q0S5zVcMgCWiWkvaxYlnrUvRd4
+j3HAIBamLkObLHirrHpGu7aE3R8WtY7R31fpXA==MIIE+DCCA+CgAwIBAgIJALURz7AYmJ5+MA0GCSqGSIb3DQEBBQUAMIIBDTEbMBkGCgmSJomT8ixk
+ARkWC0xMQU1BLlBFIFNBMQswCQYDVQQGEwJQRTENMAsGA1UECAwETElNQTENMAsGA1UEBwwETElN
+QTEYMBYGA1UECgwPVFUgRU1QUkVTQSBTLkEuMUUwQwYDVQQLDDxETkkgOTk5OTk5OSBSVUMgMTA0
+Njc3OTM1NDkgLSBDRVJUSUZJQ0FETyBQQVJBIERFTU9TVFJBQ0nDk04xRDBCBgNVBAMMO05PTUJS
+RSBSRVBSRVNFTlRBTlRFIExFR0FMIC0gQ0VSVElGSUNBRE8gUEFSQSBERU1PU1RSQUNJw5NOMRww
+GgYJKoZIhvcNAQkBFg1kZW1vQGxsYW1hLnBlMB4XDTE5MTEwODEyNTY1MFoXDTIxMTEwNzEyNTY1
+MFowggENMRswGQYKCZImiZPyLGQBGRYLTExBTUEuUEUgU0ExCzAJBgNVBAYTAlBFMQ0wCwYDVQQI
+DARMSU1BMQ0wCwYDVQQHDARMSU1BMRgwFgYDVQQKDA9UVSBFTVBSRVNBIFMuQS4xRTBDBgNVBAsM
+PEROSSA5OTk5OTk5IFJVQyAxMDQ2Nzc5MzU0OSAtIENFUlRJRklDQURPIFBBUkEgREVNT1NUUkFD
+ScOTTjFEMEIGA1UEAww7Tk9NQlJFIFJFUFJFU0VOVEFOVEUgTEVHQUwgLSBDRVJUSUZJQ0FETyBQ
+QVJBIERFTU9TVFJBQ0nDk04xHDAaBgkqhkiG9w0BCQEWDWRlbW9AbGxhbWEucGUwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCuE7cfjyUaRE1w82EDq5N3zTg2czEGRcopOBHwOV1fy9Oa
+t/Qmo0YwvFSWpIbUqVF6UdBe8jPsZY7Q2ATQNn14wRQtLtLxgy6PpFkUK50Nv9pqzEfIK+NV90ol
+QXgEg88o3Z2+MUEBDPh8j/f89x6m0irPDOX7xKvJcRsmABSaNpLY84TrlF49Egg05XVWLuXC8vMf
+RmXWyv6jK06dcAjVhGUpmGBxLm/ld5f+p9GlshB2rFyWhUBBf++g3xCwtPpyFmv+ARltScOzYcPb
+zr9fODbtHDwHDLschuGEpg8rZOwVqekyGxNW0LeDGnq6YtvcW5qVrtEELvqcNkL0D73lAgMBAAGj
+VzBVMB0GA1UdDgQWBBTe18LHVKkeRrWs3Bxp1ikK50l96jAfBgNVHSMEGDAWgBTe18LHVKkeRrWs
+3Bxp1ikK50l96jATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQUFAAOCAQEASBWcP4Ai
+qUUZSG2/Z3RU3BgvOVV3if8xYAaZT99n5PsvyBiZ3Gh6VpAW9ezoe25ZNSqGMmGfq+R4mEuiqK4h
+6jDJp0fN47IwRhWjttB9dwpxIDEkWW7zPdueGx+BY8EuyfNDWR/C7GPfu6azSHiapzeKC57AAZ8x
+o8kDdhXxDy8hTqNGolkqnc6QutW8cYPeonqyhi2THN163lZ3Cx5OV8vGFQ3ou2msF0klY9qXopI9
+i8wQjEeOG6bCvVxdID9ZjTbuGbO9pAN9hH7hZ41XEG/CspSWbFf1/Wbnlfusne9v9NgRj0MM+LAH
+M7AO5/7j1XwRq4x+U9TSVPgpoU9l5Q==
+
+
+ 2.1
+ 2.0
+ F001-1
+ 2019-12-24
+ 01
+ PEN
+
+ 12345678912
+
+
+ 12345678912
+
+
+
+
+
+
+
+ #PROJECT-OPENUBL-SIGN
+
+
+
+
+
+
+ 12345678912
+
+
+
+
+ 0000
+
+
+
+
+
+
+
+ 12121212121
+
+
+
+
+
+
+
+ FormaPago
+ Contado
+
+
+ 360.00
+
+ 2000.00
+ 360.00
+
+ S
+
+ 1000
+ IGV
+ VAT
+
+
+
+
+
+ 2000.00
+ 2360.00
+ 0
+ 2360.00
+
+
+ 1
+ 10
+ 1000.00
+
+
+ 118.00
+ 01
+
+
+
+ 180.00
+
+ 1000.00
+ 180.00
+
+ S
+ 18.00
+ 10
+
+ 1000
+ IGV
+ VAT
+
+
+
+
+
+
+
+
+ 100.00
+
+
+
+ 2
+ 10
+ 1000.00
+
+
+ 118.00
+ 01
+
+
+
+ 180.00
+
+ 1000.00
+ 180.00
+
+ S
+ 18.00
+ 10
+
+ 1000
+ IGV
+ VAT
+
+
+
+
+
+
+
+
+ 100.00
+
+
+
\ No newline at end of file
diff --git a/xsender/tests/resources/e2e/12345678912-RA-20200328-1.xml b/xsender/tests/resources/e2e/12345678912-RA-20200328-1.xml
new file mode 100644
index 00000000..9222643a
--- /dev/null
+++ b/xsender/tests/resources/e2e/12345678912-RA-20200328-1.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+ JGeIFq43Y1Ajg3sJsOCD8faxoIM=O0J36hkWE/tdwuG11wC9PUmmz7k02vcjeQuLf2RrnIV6SeHr56VTzQbYcf68XTCTOB9owyJ1HYZ5
+7r4LVTraKi5ScfAXCsPyacEF2028c/nsx4/HA4SbhAaqJSIYZ+Rz7plk1mmOE2XURyiji7EpLP5G
+bmwteXm9ZeWQTYxTT8XTHSDC7jl13GGugaYFGi5gLGMxp4AUsY+CmSgaFXIwAX4etiPjj0yIJD9V
+JrDTeTvQs6GtXAMwpW9uj6FO4KBGFg0q54u6gCl616UcLexJCL26CYHk/Pe8Vlh14OGrd184Eorb
+ZckBe3hM5mWVep5SmPTtDl7TwCWKioz0pIIxQg==MIICmzCCAYMCBgFxQLzLUjANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjAw
+NDAzMTU0OTA2WhcNMzAwNDAzMTU1MDQ2WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQCBFvkmIdnA8xd4LlEPigj/bcSYJg1KKgARd/QmTlcW80TLuYJ0
+FZuvXpf26iIT9HbkKTEkbaCqA+vCz5gIXhSiKaKuwy8FE0sagxVr93mfT/MK0x4yxijE4gyhQcY5
+4n9wQ0XaEygZT3tSm5iEgZQrcjWHAxYvfS9zGJcdumU2Q3A/wvdjiBQtMyRdYnH9bZje6EIlES/M
+SK/GwHEGNgi4j0rkpVFRSGxlYYlAj97DrX681eg6FzADWsVfFwgmHFAO4uDuT1E1SbHay3yC7d6f
+tWhD0BCe8ZC6XikVVgdcLRn8oIYNjIv9QxmnbbP0OsKUHTonj/5trxWezhwQIPuHAgMBAAEwDQYJ
+KoZIhvcNAQELBQADggEBACnf3I29sFwIV5xpxOvDNYHDapvH366kX1yBGKqnTIyWvz0bEeRuYmOa
+v0UN8gCJg+pJxoYc5NXURxTlqsj3XLirRTBW3rZJZWoING3RthU+lAMyC5Ao9rJEhNftxgSAc+DW
+5T31JR+Du6V8BnFjyTML3ZPz3QtJwC2IpaFExqjrxodWuiC6c8FtHoPZRp5QfBjdVbDN9kC9YzrP
+UkvotM3GiZNB1XrAsvxYmDa3J8ASCRJ11aGMoGkoGYQ4hGfCQBG7DDkq1REbetwE9oqUaHuTTPGi
+z3QCqD69SPpu1QuTS86iQ5dYxK70uE47yH0erb0UQ5+dNH5OMSjO8mQdGtw=
+
+
+ 2.0
+ 1.0
+ RA-20200328-1
+ 2020-03-28
+ 2020-03-28
+
+ 12312312312
+
+
+ 12312312312
+
+
+
+
+
+
+
+ #PROJECT-OPENUBL-SIGN
+
+
+
+
+ 12345678912
+ 6
+
+
+
+
+
+
+
+
+
+
+ 1
+ 01
+ F001
+ 1
+
+
+
\ No newline at end of file
diff --git a/xsender/tests/send_files.rs b/xsender/tests/send_files.rs
new file mode 100644
index 00000000..33e18f4f
--- /dev/null
+++ b/xsender/tests/send_files.rs
@@ -0,0 +1,89 @@
+use std::path::Path;
+
+use xsender::prelude::*;
+
+const BASE: &str = "tests/resources/e2e";
+
+lazy_static::lazy_static! {
+ pub static ref CLIENT: FileSender = FileSender {
+ urls: Urls {
+ invoice: "https://e-beta.sunat.gob.pe/ol-ti-itcpfegem-beta/billService".to_string(),
+ perception_retention:"https://e-beta.sunat.gob.pe/ol-ti-itemision-otroscpe-gem-beta/billService".to_string(),
+ despatch: "https://api-cpe.sunat.gob.pe/v1/contribuyente/gem".to_string(),
+ },
+ credentials: Credentials {
+ username: "12345678959MODDATOS".to_string(),
+ password: "MODDATOS".to_string(),
+ },
+ };
+}
+
+#[tokio::test]
+async fn send_invoice() {
+ let file_path = format!("{BASE}/12345678912-01-F001-1.xml");
+ let xml_file = UblFile::from_path(Path::new(&file_path)).expect("File not found");
+
+ let response = CLIENT
+ .send_file(&xml_file)
+ .await
+ .expect("Could not get a valid response");
+
+ let result = match response.data {
+ SendFileResponse::Ok(_, cdr) => {
+ assert_eq!("0", cdr.response_code);
+ assert_eq!(
+ "La Factura numero F001-1, ha sido aceptada",
+ cdr.description
+ );
+ assert_eq!(Vec::::new(), cdr.notes);
+ true
+ }
+ SendFileResponse::Ticket(_) => false,
+ SendFileResponse::Error(_, _) => false,
+ };
+
+ assert!(result);
+}
+
+#[tokio::test]
+async fn send_voided_documents() {
+ let file_path = format!("{BASE}/12345678912-RA-20200328-1.xml");
+ let xml_file = UblFile::from_path(Path::new(&file_path)).expect("File not found");
+
+ let send_file_response = CLIENT
+ .send_file(&xml_file)
+ .await
+ .expect("Could not get a valid response");
+
+ // Send file
+ let verify_ticket_target = send_file_response
+ .verify_ticket_target
+ .expect("Could not determine the verify_ticket target");
+ let ticket = match send_file_response.data {
+ SendFileResponse::Ok(_, _) => "".to_string(),
+ SendFileResponse::Ticket(ticket) => ticket,
+ SendFileResponse::Error(_, _) => "".to_string(),
+ };
+ assert!(ticket.len() > 0);
+
+ // Verify ticket
+ let verify_ticket_response = CLIENT
+ .verify_ticket(&verify_ticket_target, &ticket)
+ .await
+ .expect("Could not verify ticket");
+ let result = match verify_ticket_response {
+ VerifyTicketResponse::Ok(_, cdr, status_code) => {
+ assert_eq!("0", cdr.response_code);
+ assert_eq!(
+ "La Comunicacion de baja RA-20200328-1, ha sido aceptada",
+ cdr.description
+ );
+ assert_eq!(Vec::::new(), cdr.notes);
+
+ assert_eq!("0", status_code);
+ true
+ }
+ VerifyTicketResponse::Error(_, _) => false,
+ };
+ assert!(result);
+}