From 68f92797dba5b07a4470b27e177dc3a844834740 Mon Sep 17 00:00:00 2001 From: Jagan Date: Mon, 9 Jan 2023 12:56:03 +0530 Subject: [PATCH] feature(connector): add support for worldpay connector (#272) --- Cargo.lock | 279 +++++++- add_connector.md | 98 ++- config/Development.toml | 5 +- config/config.example.toml | 3 + config/docker_compose.toml | 5 +- connector-template/mod.rs | 87 +-- connector-template/test.rs | 2 +- crates/api_models/src/enums.rs | 1 + crates/router/Cargo.toml | 2 + crates/router/src/configs/settings.rs | 1 + crates/router/src/connector.rs | 6 +- crates/router/src/connector/shift4.rs | 15 + crates/router/src/connector/worldpay.rs | 613 ++++++++++++++++++ .../router/src/connector/worldpay/requests.rs | 225 +++++++ .../router/src/connector/worldpay/response.rs | 306 +++++++++ .../src/connector/worldpay/transformers.rs | 179 +++++ crates/router/src/core/errors.rs | 4 +- .../router/src/core/payments/transformers.rs | 1 + crates/router/src/core/utils.rs | 1 + crates/router/src/services/api.rs | 2 +- crates/router/src/types.rs | 1 + crates/router/src/types/api.rs | 1 + crates/router/tests/connectors/aci.rs | 2 + .../tests/connectors/authorizedotnet.rs | 2 + crates/router/tests/connectors/checkout.rs | 2 + .../router/tests/connectors/connector_auth.rs | 1 + crates/router/tests/connectors/main.rs | 1 + .../router/tests/connectors/sample_auth.toml | 3 + crates/router/tests/connectors/utils.rs | 102 ++- crates/router/tests/connectors/worldpay.rs | 320 +++++++++ scripts/add_connector.sh | 32 +- 31 files changed, 2198 insertions(+), 104 deletions(-) create mode 100644 crates/router/src/connector/worldpay.rs create mode 100644 crates/router/src/connector/worldpay/requests.rs create mode 100644 crates/router/src/connector/worldpay/response.rs create mode 100644 crates/router/src/connector/worldpay/transformers.rs create mode 100644 crates/router/tests/connectors/worldpay.rs diff --git a/Cargo.lock b/Cargo.lock index d806bae97a..fb6349e548 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand", + "rand 0.8.5", "sha1", "smallvec", "tracing", @@ -263,7 +263,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom", + "getrandom 0.2.8", "once_cell", "version_check", ] @@ -334,6 +334,16 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f907281554a3d0312bb7aab855a8e0ef6cbf1614d06de54105039ca8b34460e" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-bb8-diesel" version = "0.1.0" @@ -346,6 +356,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + [[package]] name = "async-stream" version = "0.3.3" @@ -422,7 +443,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand", + "rand 0.8.5", "rustls 0.20.7", "serde", "serde_json", @@ -936,6 +957,15 @@ dependencies = [ "time", ] +[[package]] +name = "concurrent-queue" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.13.3" @@ -1069,6 +1099,25 @@ dependencies = [ "parking_lot_core 0.9.5", ] +[[package]] +name = "deadpool" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "retain_mut", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaa37046cc0f6c3cc6090fbdbf73ef0b8ef4cfcc37f6befc0020f63e8cf121e1" + [[package]] name = "derive_deref" version = "1.1.1" @@ -1231,13 +1280,19 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fake" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d68f517805463f3a896a9d29c1d6ff09d3579ded64a7201b4069f8f9c0d52fd" dependencies = [ - "rand", + "rand 0.8.5", ] [[package]] @@ -1317,7 +1372,7 @@ dependencies = [ "native-tls", "parking_lot 0.11.2", "pretty_env_logger", - "rand", + "rand 0.8.5", "redis-protocol", "semver", "sha-1", @@ -1442,6 +1497,21 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.25" @@ -1465,6 +1535,12 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.25" @@ -1503,6 +1579,17 @@ dependencies = [ "windows", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.8" @@ -1511,7 +1598,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -1625,6 +1712,27 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel", + "base64 0.13.1", + "futures-lite", + "http", + "infer", + "pin-project-lite", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs 0.8.5", + "serde_urlencoded", + "url", +] + [[package]] name = "httparse" version = "1.8.0" @@ -1732,6 +1840,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + [[package]] name = "instant" version = "0.1.12" @@ -2017,7 +2131,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.42.0", ] @@ -2027,7 +2141,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" dependencies = [ - "rand", + "rand 0.8.5", ] [[package]] @@ -2231,7 +2345,7 @@ dependencies = [ "once_cell", "opentelemetry_api", "percent-encoding", - "rand", + "rand 0.8.5", "thiserror", "tokio", "tokio-stream", @@ -2262,6 +2376,12 @@ dependencies = [ "supports-color", ] +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" version = "0.11.2" @@ -2486,8 +2606,8 @@ dependencies = [ "lazy_static", "num-traits", "quick-error 2.0.1", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -2549,6 +2669,19 @@ dependencies = [ "scheduled-thread-pool", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -2556,8 +2689,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -2567,7 +2710,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -2576,7 +2728,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.8", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -2585,7 +2746,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -2697,6 +2858,12 @@ dependencies = [ "winreg", ] +[[package]] +name = "retain_mut" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" + [[package]] name = "ring" version = "0.16.20" @@ -2763,7 +2930,7 @@ dependencies = [ "nanoid", "num_cpus", "once_cell", - "rand", + "rand 0.8.5", "redis_interface", "reqwest", "ring", @@ -2772,8 +2939,9 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", - "serde_qs", + "serde_qs 0.10.1", "serde_urlencoded", + "serial_test", "storage_models", "structopt", "strum", @@ -2783,6 +2951,7 @@ dependencies = [ "toml", "url", "uuid", + "wiremock", ] [[package]] @@ -3024,6 +3193,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_qs" version = "0.10.1" @@ -3047,6 +3227,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c789ec87f4687d022a2405cf46e0cd6284889f1839de292cadeb6c6019506f2" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot 0.12.1", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b64f9e531ce97c88b4778aad0ceee079216071cffec6ac9b904277f8f92e7fe3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -3508,7 +3713,7 @@ dependencies = [ "indexmap", "pin-project", "pin-project-lite", - "rand", + "rand 0.8.5", "slab", "tokio", "tokio-util 0.7.4", @@ -3754,7 +3959,7 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ - "getrandom", + "getrandom 0.2.8", "serde", ] @@ -3809,6 +4014,12 @@ dependencies = [ "libc", ] +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "want" version = "0.3.0" @@ -3819,6 +4030,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -4085,6 +4302,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "wiremock" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249dc68542861d17eae4b4e5e8fb381c2f9e8f255a84f6771d5fdf8b6c03ce3c" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64 0.13.1", + "deadpool", + "futures", + "futures-timer", + "http-types", + "hyper", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "xmlparser" version = "0.13.3" diff --git a/add_connector.md b/add_connector.md index 14868530fb..90837ce6cc 100644 --- a/add_connector.md +++ b/add_connector.md @@ -45,36 +45,29 @@ Below is a step-by-step tutorial for integrating a new connector. ### **Generate the template** -Install cargo generate. - ```bash -cargo install cargo-generate -``` - -Under `crates/router/src/connector/` run the following commands - -```bash -cargo gen-pg +cd scripts +sh add_connector.sh ``` For this tutorial `` would be `checkout`. -The folder structure will be modified this way +The folder structure will be modified as below +``` +crates/router/src/connector +├── checkout +│ └── transformers.rs +└── checkout.rs +crates/router/tests/connectors +└── checkout.rs +``` -![directory image](/docs/imgs/connector-layout.png) - -`transformers.rs` will contain connectors API Request and Response types, and conversion between the router and connector API types. -`mod.rs` will contain the trait implementations for the connector. +`crates/router/src/connector/checkout/transformers.rs` will contain connectors API Request and Response types, and conversion between the router and connector API types. +`crates/router/src/connector/checkout.rs` will contain the trait implementations for the connector. +`crates/router/tests/connectors/checkout.rs` will contain the basic tests for the payments flows. There is boiler plate code with `todo!()` in the above mentioned files. Go through the rest of the guide and fill in code wherever necessary. -Add the below lines in `src/connector/mod.rs` - -```rust -pub mod checkout; -pub use checkout::Checkout; -``` - ### **Implementing Request and Response types** Adding new Connector is all about implementing the data transformation from Router's core to Connector's API request format. @@ -287,13 +280,64 @@ Don’t forget to add logs lines in appropriate places. Refer to other conector code for trait implementations. mostly tThe rust compiler will guide you to do it easily. Feel free to connect with us in case of any queries and if you want to confirm the status mapping. -Feel free to connect with us in case of queries and also if you want to confirm the status mapping. +### **Test the connector** +Try running the tests in `crates/router/tests/connectors/{{connector-name}}.rs`. +All tests should pass and add appropiate tests for connector specific payment flows. -### After implementing the above +### **Build payment request and response from json schema** +Some connectors will provide [json schema](https://developer.worldpay.com/docs/access-worldpay/api/references/payments) for each request and response supported. We can directly convert that schema to rust code by using below script. On running the script a `temp.rs` file will be created in `src/connector/` folder -Add connector name in : +*Note: The code generated may not be production ready and might fail for some case, we have to clean up the code as per our standards.* -- `crates/api_models/src/enums.rs` in Connector enum (in alphabetical order) -- `crates/router/src/types/api/mod.rs` convert_connector function +```bash +brew install openapi-generator +export CONNECTOR_NAME="" #Change it to appropriate connector name +export SCHEMA_PATH="" #it can be json or yaml, Refer samples below +openapi-generator generate -g rust -i ${SCHEMA_PATH} -o temp && cat temp/src/models/* > crates/router/src/connector/${CONNECTOR_NAME}/temp.rs && rm -rf temp && sed -i'' -r "s/^pub use.*//;s/^pub mod.*//;s/^\/.*//;s/^.\*.*//;s/crate::models:://g;" crates/router/src/connector/${CONNECTOR_NAME}/temp.rs && cargo +nightly fmt +``` -Configure the Connectors API credentials using the PaymentConnectors API. +JSON example +```json +{ + "openapi": "3.0.1", + "paths": {}, + "info": { + "title": "", + "version": "" + }, + "components": { + "schemas": { + "PaymentsResponse": { + "type": "object", + "properties": { + "outcome": { + "type": "string" + } + }, + "required": [ + "outcome" + ] + } + } + } +} +``` + +YAML example +```yaml +--- +openapi: 3.0.1 +paths: {} +info: + title: "" + version: "" +components: + schemas: + PaymentsResponse: + type: object + properties: + outcome: + type: string + required: + - outcome +``` diff --git a/config/Development.toml b/config/Development.toml index b80cba72e9..456311468b 100644 --- a/config/Development.toml +++ b/config/Development.toml @@ -38,7 +38,7 @@ locker_decryption_key2 = "" [connectors.supported] wallets = ["klarna", "braintree", "applepay"] -cards = ["stripe", "adyen", "authorizedotnet", "checkout", "braintree", "aci", "shift4", "cybersource"] +cards = ["stripe", "adyen", "authorizedotnet", "checkout", "braintree", "aci", "shift4", "cybersource", "worldpay"] [eph_key] validity = 1 @@ -73,6 +73,9 @@ base_url = "https://apitest.cybersource.com/" [connectors.shift4] base_url = "https://api.shift4.com/" +[connectors.worldpay] +base_url = "http://localhost:9090/" + [scheduler] stream = "SCHEDULER_STREAM" consumer_group = "SCHEDULER_GROUP" diff --git a/config/config.example.toml b/config/config.example.toml index 8487c71440..8f8b607f7b 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -125,6 +125,9 @@ base_url = "https://apitest.cybersource.com/" [connectors.shift4] base_url = "https://api.shift4.com/" +[connectors.worldpay] +base_url = "https://try.access.worldpay.com/" + # This data is used to call respective connectors for wallets and cards [connectors.supported] wallets = ["klarna", "braintree", "applepay"] diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 26b72c0a2f..1be7c770bd 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -80,6 +80,9 @@ base_url = "https://apitest.cybersource.com/" [connectors.shift4] base_url = "https://api.shift4.com/" +[connectors.worldpay] +base_url = "https://try.access.worldpay.com/" + [connectors.supported] wallets = ["klarna", "braintree", "applepay"] -cards = ["stripe", "adyen", "authorizedotnet", "checkout", "braintree", "shift4", "cybersource"] +cards = ["stripe", "adyen", "authorizedotnet", "checkout", "braintree", "shift4", "cybersource", "worldpay"] diff --git a/connector-template/mod.rs b/connector-template/mod.rs index 4cf6645993..4081b1b72b 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -3,7 +3,7 @@ mod transformers; use std::fmt::Debug; use bytes::Bytes; -use error_stack::ResultExt; +use error_stack::{ResultExt, IntoReport}; use crate::{ configs::settings, @@ -12,7 +12,7 @@ use crate::{ errors::{self, CustomResult}, payments, }, - headers, logger, services, + headers, logger, services::{self, ConnectorIntegration}, types::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, @@ -26,16 +26,19 @@ use transformers as {{project-name | downcase}}; #[derive(Debug, Clone)] pub struct {{project-name | downcase | pascal_case}}; -impl api::ConnectorCommonExt for {{project-name | downcase | pascal_case}} { - fn build_headers( +impl ConnectorCommonExt for {{project-name | downcase | pascal_case}} +where + Self: ConnectorIntegration,{ + fn build_headers( &self, - req: &types::RouterData, + _req: &types::RouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { todo!() } } -impl api::ConnectorCommon for {{project-name | downcase | pascal_case}} { +impl ConnectorCommon for {{project-name | downcase | pascal_case}} { fn id(&self) -> &'static str { "{{project-name | downcase}}" } @@ -58,7 +61,7 @@ impl api::Payment for {{project-name | downcase | pascal_case}} {} impl api::PreVerify for {{project-name | downcase | pascal_case}} {} impl - services::ConnectorIntegration< + ConnectorIntegration< api::Verify, types::VerifyRequestData, types::PaymentsResponseData, @@ -69,7 +72,7 @@ impl impl api::PaymentVoid for {{project-name | downcase | pascal_case}} {} impl - services::ConnectorIntegration< + ConnectorIntegration< api::Void, types::PaymentsCancelData, types::PaymentsResponseData, @@ -78,12 +81,13 @@ impl impl api::PaymentSync for {{project-name | downcase | pascal_case}} {} impl - services::ConnectorIntegration + ConnectorIntegration for {{project-name | downcase | pascal_case}} { fn get_headers( &self, - req: &types::PaymentsSyncRouterData, + _req: &types::PaymentsSyncRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { todo!() } @@ -94,31 +98,31 @@ impl fn get_url( &self, - req: &types::PaymentsSyncRouterData, - connectors: &settings::Connectors, + _req: &types::PaymentsSyncRouterData, + _connectors: &settings::Connectors, ) -> CustomResult { todo!() } fn build_request( &self, - req: &types::PaymentsSyncRouterData, - connectors: &settings::Connectors, + _req: &types::PaymentsSyncRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { todo!() } fn get_error_response( &self, - res: Bytes, + _res: Bytes, ) -> CustomResult { todo!() } fn handle_response( &self, - data: &types::PaymentsSyncRouterData, - res: Response, + _data: &types::PaymentsSyncRouterData, + _res: Response, ) -> CustomResult { todo!() } @@ -127,7 +131,7 @@ impl impl api::PaymentCapture for {{project-name | downcase | pascal_case}} {} impl - services::ConnectorIntegration< + ConnectorIntegration< api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData, @@ -135,7 +139,8 @@ impl { fn get_headers( &self, - req: &types::PaymentsCaptureRouterData, + _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { todo!() } @@ -153,31 +158,31 @@ impl fn build_request( &self, - req: &types::PaymentsCaptureRouterData, - connectors: &settings::Connectors, + _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { todo!() } fn handle_response( &self, - data: &types::PaymentsCaptureRouterData, - res: Response, + _data: &types::PaymentsCaptureRouterData, + _res: Response, ) -> CustomResult { todo!() } fn get_url( &self, - req: &types::PaymentsCaptureRouterData, - connectors: &settings::Connectors, + _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult { todo!() } fn get_error_response( &self, - res: Bytes, + _res: Bytes, ) -> CustomResult { todo!() } @@ -186,7 +191,7 @@ impl impl api::PaymentSession for {{project-name | downcase | pascal_case}} {} impl - services::ConnectorIntegration< + ConnectorIntegration< api::Session, types::PaymentsSessionData, types::PaymentsResponseData, @@ -198,12 +203,12 @@ impl impl api::PaymentAuthorize for {{project-name | downcase | pascal_case}} {} impl - services::ConnectorIntegration< + ConnectorIntegration< api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData, > for {{project-name | downcase | pascal_case}} { - fn get_headers(&self, _req: &types::PaymentsAuthorizeRouterData) -> CustomResult,errors::ConnectorError> { + fn get_headers(&self, _req: &types::PaymentsAuthorizeRouterData,_connectors: &settings::Connectors,) -> CustomResult,errors::ConnectorError> { todo!() } @@ -211,13 +216,13 @@ impl todo!() } - fn get_url(&self, _req: &types::PaymentsAuthorizeRouterData, connectors: &settings::Connectors,) -> CustomResult { + fn get_url(&self, _req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors,) -> CustomResult { todo!() } fn get_request_body(&self, req: &types::PaymentsAuthorizeRouterData) -> CustomResult,errors::ConnectorError> { let {{project-name | downcase}}_req = - utils::Encode::<{{project-name | downcase}}::{{project-name | downcase | pascal_case}}PaymentsRequest>::convert_and_url_encode(req).change_context(errors::ConnectorError::RequestEncodingFailed)?; + utils::Encode::<{{project-name | downcase}}::{{project-name | downcase | pascal_case}}PaymentsRequest>::convert_and_encode(req).change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some({{project-name | downcase}}_req)) } @@ -247,12 +252,12 @@ impl api::RefundExecute for {{project-name | downcase | pascal_case}} {} impl api::RefundSync for {{project-name | downcase | pascal_case}} {} impl - services::ConnectorIntegration< + ConnectorIntegration< api::Execute, types::RefundsData, types::RefundsResponseData, > for {{project-name | downcase | pascal_case}} { - fn get_headers(&self, _req: &types::RefundsRouterData) -> CustomResult,errors::ConnectorError> { + fn get_headers(&self, _req: &types::RefundsRouterData,_connectors: &settings::Connectors,) -> CustomResult,errors::ConnectorError> { todo!() } @@ -260,12 +265,12 @@ impl todo!() } - fn get_url(&self, _req: &types::RefundsRouterData, connectors: &settings::Connectors,) -> CustomResult { + fn get_url(&self, _req: &types::RefundsRouterData, _connectors: &settings::Connectors,) -> CustomResult { todo!() } fn get_request_body(&self, req: &types::RefundsRouterData) -> CustomResult,errors::ConnectorError> { - let {{project-name | downcase}}_req = utils::Encode::<{{project-name| downcase}}::{{project-name | downcase | pascal_case}}RefundRequest>::convert_and_url_encode(req).change_context(errors::ConnectorError::RequestEncodingFailed)?; + let {{project-name | downcase}}_req = utils::Encode::<{{project-name| downcase}}::{{project-name | downcase | pascal_case}}RefundRequest>::convert_and_encode(req).change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some({{project-name | downcase}}_req)) } @@ -273,7 +278,7 @@ impl let request = services::RequestBuilder::new() .method(services::Method::Post) .url(&types::RefundExecuteType::get_url(self, req, connectors)?) - .headers(types::RefundExecuteType::get_headers(self, req)?) + .headers(types::RefundExecuteType::get_headers(self, req, connectors)?) .body(types::RefundExecuteType::get_request_body(self, req)?) .build(); Ok(Some(request)) @@ -301,8 +306,8 @@ impl } impl - services::ConnectorIntegration for {{project-name | downcase | pascal_case}} { - fn get_headers(&self, _req: &types::RefundSyncRouterData) -> CustomResult,errors::ConnectorError> { + ConnectorIntegration for {{project-name | downcase | pascal_case}} { + fn get_headers(&self, _req: &types::RefundSyncRouterData,_connectors: &settings::Connectors,) -> CustomResult,errors::ConnectorError> { todo!() } @@ -341,21 +346,21 @@ impl api::IncomingWebhook for {{project-name | downcase | pascal_case}} { &self, _body: &[u8], ) -> CustomResult { - todo!() + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } fn get_webhook_event_type( &self, _body: &[u8], ) -> CustomResult { - todo!() + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } fn get_webhook_resource_object( &self, _body: &[u8], ) -> CustomResult { - todo!() + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/connector-template/test.rs b/connector-template/test.rs index 1ba4de5e6f..a5d8a15e1c 100644 --- a/connector-template/test.rs +++ b/connector-template/test.rs @@ -8,7 +8,7 @@ use crate::{ }; struct {{project-name | downcase | pascal_case}}; -impl utils::ConnectorActions for {{project-name | downcase | pascal_case}} {} +impl ConnectorActions for {{project-name | downcase | pascal_case}} {} impl utils::Connector for {{project-name | downcase | pascal_case}} { fn get_data(&self) -> types::api::ConnectorData { use router::connector::{{project-name | downcase | pascal_case}}; diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 48c5a32a9d..459fb30a6d 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -497,6 +497,7 @@ pub enum Connector { Klarna, Shift4, Stripe, + Worldpay, } impl From for IntentStatus { diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index e65decd6db..511fbab78c 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -87,6 +87,8 @@ rand = "0.8.5" time = { version = "0.3.17", features = ["macros"] } tokio = "1.23.0" toml = "0.5.9" +serial_test = "0.10.0" +wiremock = "0.5" [[bin]] name = "router" diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index f5e8e74431..872512ded5 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -117,6 +117,7 @@ pub struct Connectors { pub shift4: ConnectorParams, pub stripe: ConnectorParams, pub supported: SupportedConnectors, + pub worldpay: ConnectorParams, pub applepay: ConnectorParams, } diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 9b598df8f3..af3f33990e 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -6,12 +6,12 @@ pub mod braintree; pub mod checkout; pub mod cybersource; pub mod klarna; -pub mod stripe; - pub mod shift4; +pub mod stripe; +pub mod worldpay; pub use self::{ aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet, braintree::Braintree, checkout::Checkout, cybersource::Cybersource, klarna::Klarna, - shift4::Shift4, stripe::Stripe, + shift4::Shift4, stripe::Stripe, worldpay::Worldpay, }; diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index 312a525910..3f76f0aef6 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -439,6 +439,21 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .body(types::RefundSyncType::get_request_body(self, req)?) + .build(), + )) + } + fn handle_response( &self, data: &types::RefundSyncRouterData, diff --git a/crates/router/src/connector/worldpay.rs b/crates/router/src/connector/worldpay.rs new file mode 100644 index 0000000000..e2d9741e9d --- /dev/null +++ b/crates/router/src/connector/worldpay.rs @@ -0,0 +1,613 @@ +mod requests; +mod response; +mod transformers; + +use std::fmt::Debug; + +use bytes::Bytes; +use error_stack::{IntoReport, ResultExt}; +use storage_models::enums; +use transformers as worldpay; + +use self::{requests::*, response::*}; +use crate::{ + configs::settings, + core::{ + errors::{self, CustomResult}, + payments, + }, + headers, logger, + services::{self, ConnectorIntegration}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Worldpay; + +impl ConnectorCommonExt for Worldpay +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let mut headers = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + headers.append(&mut api_key); + Ok(headers) + } +} + +impl ConnectorCommon for Worldpay { + fn id(&self) -> &'static str { + "worldpay" + } + + fn common_get_content_type(&self) -> &'static str { + "application/vnd.worldpay.payments-v6+json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.worldpay.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult, errors::ConnectorError> { + let auth: worldpay::WorldpayAuthType = auth_type + .try_into() + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![(headers::AUTHORIZATION.to_string(), auth.api_key)]) + } + + fn build_error_response( + &self, + res: Bytes, + ) -> CustomResult { + let response: WorldpayErrorResponse = res + .parse_struct("WorldpayErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(ErrorResponse { + code: response.error_name, + message: response.message, + reason: None, + }) + } +} + +impl api::Payment for Worldpay {} + +impl api::PreVerify for Worldpay {} +impl ConnectorIntegration + for Worldpay +{ +} + +impl api::PaymentVoid for Worldpay {} + +impl ConnectorIntegration + for Worldpay +{ + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}payments/settlements/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult + where + api::Void: Clone, + types::PaymentsCancelData: Clone, + types::PaymentsResponseData: Clone, + { + match res.status_code { + 202 => { + let response: WorldpayPaymentsResponse = res + .response + .parse_struct("Worldpay PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(types::PaymentsCancelRouterData { + status: enums::AttemptStatus::Voided, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::try_from(response.links)?, + redirection_data: None, + redirect: false, + mandate_reference: None, + }), + ..data.clone() + }) + } + _ => Err(errors::ConnectorError::ResponseHandlingFailed)?, + } + } + + fn get_error_response( + &self, + res: Bytes, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::PaymentSync for Worldpay {} +impl ConnectorIntegration + for Worldpay +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}payments/events/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .body(types::PaymentsSyncType::get_request_body(self, req)?) + .build(), + )) + } + + fn get_error_response( + &self, + res: Bytes, + ) -> CustomResult { + self.build_error_response(res) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + let response: WorldpayEventResponse = + res.response + .parse_struct("Worldpay EventResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(types::PaymentsSyncRouterData { + status: enums::AttemptStatus::from(response.last_event), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: data.request.connector_transaction_id.clone(), + redirection_data: None, + redirect: false, + mandate_reference: None, + }), + ..data.clone() + }) + } +} + +impl api::PaymentCapture for Worldpay {} +impl ConnectorIntegration + for Worldpay +{ + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: Response, + ) -> CustomResult { + logger::debug!(worldpaypayments_capture_response=?res); + match res.status_code { + 202 => { + let response: WorldpayPaymentsResponse = res + .response + .parse_struct("Worldpay PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(types::PaymentsCaptureRouterData { + status: enums::AttemptStatus::Charged, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::try_from(response.links)?, + redirection_data: None, + redirect: false, + mandate_reference: None, + }), + ..data.clone() + }) + } + _ => Err(errors::ConnectorError::ResponseHandlingFailed)?, + } + } + + fn get_url( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}payments/settlements/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn get_error_response( + &self, + res: Bytes, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::PaymentSession for Worldpay {} + +impl ConnectorIntegration + for Worldpay +{ +} + +impl api::PaymentAuthorize for Worldpay {} + +impl ConnectorIntegration + for Worldpay +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}payments/authorizations", + self.base_url(connectors) + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let worldpay_req = utils::Encode::::convert_and_encode(req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(worldpay_req)) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: WorldpayPaymentsResponse = res + .response + .parse_struct("Worldpay PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + logger::debug!(worldpaypayments_create_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Bytes, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::Refund for Worldpay {} +impl api::RefundExecute for Worldpay {} +impl api::RefundSync for Worldpay {} + +impl ConnectorIntegration + for Worldpay +{ + fn get_headers( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_request_body( + &self, + req: &types::RefundExecuteRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req = utils::Encode::::convert_and_encode(req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(req)) + } + + fn get_url( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}payments/settlements/refunds/partials/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .body(types::RefundExecuteType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + logger::debug!(target: "router::connector::worldpay", response=?res); + match res.status_code { + 202 => { + let response: WorldpayPaymentsResponse = res + .response + .parse_struct("Worldpay PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(types::RefundExecuteRouterData { + response: Ok(types::RefundsResponseData { + connector_refund_id: ResponseIdStr::try_from(response.links)?.id, + refund_status: enums::RefundStatus::Success, + }), + ..data.clone() + }) + } + _ => Err(errors::ConnectorError::ResponseHandlingFailed)?, + } + } + + fn get_error_response( + &self, + res: Bytes, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration for Worldpay { + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}payments/events/{}", + self.base_url(connectors), + req.request.connector_transaction_id + )) + } + + fn build_request( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .body(types::RefundSyncType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + let response: WorldpayEventResponse = + res.response + .parse_struct("Worldpay EventResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(types::RefundSyncRouterData { + response: Ok(types::RefundsResponseData { + connector_refund_id: data.request.refund_id.clone(), + refund_status: enums::RefundStatus::from(response.last_event), + }), + ..data.clone() + }) + } + + fn get_error_response( + &self, + res: Bytes, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Worldpay { + fn get_webhook_object_reference_id( + &self, + _body: &[u8], + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _body: &[u8], + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _body: &[u8], + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} + +impl services::ConnectorRedirectResponse for Worldpay { + fn get_flow_type( + &self, + _query_params: &str, + ) -> CustomResult { + Ok(payments::CallConnectorAction::Trigger) + } +} diff --git a/crates/router/src/connector/worldpay/requests.rs b/crates/router/src/connector/worldpay/requests.rs new file mode 100644 index 0000000000..a76b02d7cf --- /dev/null +++ b/crates/router/src/connector/worldpay/requests.rs @@ -0,0 +1,225 @@ +use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BillingAddress { + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub address2: Option, + pub postal_code: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub address3: Option, + pub country_code: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub address1: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorldpayPaymentsRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub channel: Option, + pub instruction: Instruction, + #[serde(skip_serializing_if = "Option::is_none")] + pub customer: Option, + pub merchant: Merchant, + pub transaction_reference: String, +} + +#[derive( + Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, +)] +#[serde(rename_all = "camelCase")] +pub enum Channel { + #[default] + Moto, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Customer { + #[serde(skip_serializing_if = "Option::is_none")] + pub risk_profile: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub authentication: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum CustomerAuthentication { + ThreeDS(ThreeDS), + Token(NetworkToken), +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ThreeDS { + #[serde(skip_serializing_if = "Option::is_none")] + pub authentication_value: Option, + pub version: ThreeDSVersion, + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_id: Option, + pub eci: String, + #[serde(rename = "type")] + pub auth_type: CustomerAuthType, +} + +#[derive( + Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, +)] +pub enum ThreeDSVersion { + #[default] + #[serde(rename = "1")] + One, + #[serde(rename = "2")] + Two, +} + +#[derive( + Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, +)] +pub enum CustomerAuthType { + #[serde(rename = "3DS")] + #[default] + Variant3Ds, + #[serde(rename = "card/networkToken")] + NetworkToken, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetworkToken { + #[serde(rename = "type")] + pub auth_type: CustomerAuthType, + pub authentication_value: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub eci: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Instruction { + #[serde(skip_serializing_if = "Option::is_none")] + pub debt_repayment: Option, + pub value: PaymentValue, + pub narrative: InstructionNarrative, + pub payment_instrument: PaymentInstrument, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstructionNarrative { + pub line1: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub line2: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PaymentInstrument { + Card(CardPayment), + CardToken(CardToken), + Googlepay(WalletPayment), + Applepay(WalletPayment), +} + +#[derive( + Clone, Copy, Debug, Eq, Default, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, +)] +pub enum PaymentType { + #[default] + #[serde(rename = "card/plain")] + Card, + #[serde(rename = "card/token")] + CardToken, + #[serde(rename = "card/wallet+googlepay")] + Googlepay, + #[serde(rename = "card/wallet+applepay")] + Applepay, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardPayment { + #[serde(skip_serializing_if = "Option::is_none")] + pub billing_address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub card_holder_name: Option, + pub card_expiry_date: CardExpiryDate, + #[serde(skip_serializing_if = "Option::is_none")] + pub cvc: Option, + #[serde(rename = "type")] + pub payment_type: PaymentType, + pub card_number: String, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardToken { + #[serde(rename = "type")] + pub payment_type: PaymentType, + pub href: String, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletPayment { + #[serde(rename = "type")] + pub payment_type: PaymentType, + pub wallet_token: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub billing_address: Option, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct CardExpiryDate { + pub month: u8, + pub year: u16, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct PaymentValue { + pub amount: i64, + pub currency: String, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Merchant { + pub entity: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub mcc: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_facilitator: Option, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentFacilitator { + pub pf_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub iso_id: Option, + pub sub_merchant: SubMerchant, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubMerchant { + pub city: String, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + pub postal_code: String, + pub merchant_id: String, + pub country_code: String, + pub street: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub tax_id: Option, +} + +#[derive(Default, Debug, Serialize)] +pub struct WorldpayRefundRequest { + pub value: PaymentValue, + pub reference: String, +} diff --git a/crates/router/src/connector/worldpay/response.rs b/crates/router/src/connector/worldpay/response.rs new file mode 100644 index 0000000000..5102ac9528 --- /dev/null +++ b/crates/router/src/connector/worldpay/response.rs @@ -0,0 +1,306 @@ +use serde::{Deserialize, Serialize}; + +use crate::{core::errors, types}; +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorldpayPaymentsResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub exemption: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub issuer: Option, + pub outcome: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_instrument: Option, + /// Any risk factors which have been identified for the authorization. This section will not appear if no risks are identified. + #[serde(skip_serializing_if = "Option::is_none")] + pub risk_factors: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub scheme: Option, + #[serde(rename = "_links", skip_serializing_if = "Option::is_none")] + pub links: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum Outcome { + Authorized, + Refused, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorldpayEventResponse { + pub last_event: EventType, + #[serde(rename = "_links", skip_serializing_if = "Option::is_none")] + pub links: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum EventType { + Authorized, + Cancelled, + Charged, + SentForRefund, + RefundFailed, + Refused, + Refunded, + Error, + CaptureFailed, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct Exemption { + pub result: String, + pub reason: String, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct PaymentLinks { + #[serde(rename = "payments:events", skip_serializing_if = "Option::is_none")] + pub events: Option, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct EventLinks { + #[serde(rename = "payments:events", skip_serializing_if = "Option::is_none")] + pub events: Option, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct PaymentLink { + pub href: String, +} + +fn get_resource_id( + links: Option, + transform_fn: F, +) -> Result> +where + F: Fn(String) -> T, +{ + let reference_id = links + .and_then(|l| l.events) + .and_then(|e| e.href.rsplit_once('/').map(|h| h.1.to_string())) + .map(transform_fn); + reference_id.ok_or_else(|| { + errors::ConnectorError::MissingRequiredField { + field_name: "links.events".to_string(), + } + .into() + }) +} + +pub struct ResponseIdStr { + pub id: String, +} + +impl TryFrom> for ResponseIdStr { + type Error = error_stack::Report; + fn try_from(links: Option) -> Result { + get_resource_id(links, |id| Self { id }) + } +} + +impl TryFrom> for types::ResponseId { + type Error = error_stack::Report; + fn try_from(links: Option) -> Result { + get_resource_id(links, Self::ConnectorTransactionId) + } +} + +impl Exemption { + pub fn new(result: String, reason: String) -> Self { + Self { result, reason } + } +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Issuer { + pub authorization_code: String, +} + +impl Issuer { + pub fn new(authorization_code: String) -> Self { + Self { authorization_code } + } +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct PaymentsResPaymentInstrument { + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub risk_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub card: Option, +} + +impl PaymentsResPaymentInstrument { + pub fn new() -> Self { + Self { + risk_type: None, + card: None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentInstrumentCard { + #[serde(skip_serializing_if = "Option::is_none")] + pub number: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub issuer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_account_reference: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub country_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub funding_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub brand: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub expiry_date: Option, +} + +impl PaymentInstrumentCard { + pub fn new() -> Self { + Self { + number: None, + issuer: None, + payment_account_reference: None, + country_code: None, + funding_type: None, + brand: None, + expiry_date: None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentInstrumentCardExpiryDate { + #[serde(skip_serializing_if = "Option::is_none")] + pub month: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub year: Option, +} + +impl PaymentInstrumentCardExpiryDate { + pub fn new() -> Self { + Self { + month: None, + year: None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentInstrumentCardIssuer { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl PaymentInstrumentCardIssuer { + pub fn new() -> Self { + Self { name: None } + } +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentInstrumentCardNumber { + #[serde(skip_serializing_if = "Option::is_none")] + pub bin: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last4_digits: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dpan: Option, +} + +impl PaymentInstrumentCardNumber { + pub fn new() -> Self { + Self { + bin: None, + last4_digits: None, + dpan: None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RiskFactorsInner { + #[serde(rename = "type")] + pub risk_type: RiskType, + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + pub risk: Risk, +} + +impl RiskFactorsInner { + pub fn new(risk_type: RiskType, risk: Risk) -> Self { + Self { + risk_type, + detail: None, + risk, + } + } +} + +#[derive( + Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, +)] +#[serde(rename_all = "camelCase")] +pub enum RiskType { + #[default] + Avs, + Cvc, + RiskProfile, +} + +#[derive( + Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, +)] +#[serde(rename_all = "camelCase")] +pub enum Detail { + #[default] + Address, + Postcode, +} + +#[derive( + Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, +)] +pub enum Risk { + #[default] + #[serde(rename = "not_checked")] + NotChecked, + #[serde(rename = "not_matched")] + NotMatched, + #[serde(rename = "not_supplied")] + NotSupplied, + #[serde(rename = "verificationFailed")] + VerificationFailed, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct PaymentsResponseScheme { + pub reference: String, +} + +impl PaymentsResponseScheme { + pub fn new(reference: String) -> Self { + Self { reference } + } +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct WorldpayErrorResponse { + pub error_name: String, + pub message: String, +} diff --git a/crates/router/src/connector/worldpay/transformers.rs b/crates/router/src/connector/worldpay/transformers.rs new file mode 100644 index 0000000000..0ca5da3c78 --- /dev/null +++ b/crates/router/src/connector/worldpay/transformers.rs @@ -0,0 +1,179 @@ +use std::str::FromStr; + +use common_utils::errors::CustomResult; +use masking::PeekInterface; +use storage_models::enums; + +use super::{requests::*, response::*}; +use crate::{ + core::errors, + types::{self, api}, +}; + +fn parse_int( + val: masking::Secret, +) -> CustomResult +where + ::Err: Sync, +{ + let res = val.peek().parse::(); + if let Ok(val) = res { + Ok(val) + } else { + Err(errors::ConnectorError::RequestEncodingFailed)? + } +} + +fn fetch_payment_instrument( + payment_method: api::PaymentMethod, +) -> CustomResult { + match payment_method { + api::PaymentMethod::Card(card) => Ok(PaymentInstrument::Card(CardPayment { + card_expiry_date: CardExpiryDate { + month: parse_int::(card.card_exp_month)?, + year: parse_int::(card.card_exp_year)?, + }, + card_number: card.card_number.peek().to_string(), + ..CardPayment::default() + })), + api::PaymentMethod::Wallet(wallet) => match wallet.issuer_name { + api_models::enums::WalletIssuer::ApplePay => { + Ok(PaymentInstrument::Applepay(WalletPayment { + payment_type: PaymentType::Applepay, + wallet_token: wallet.token, + ..WalletPayment::default() + })) + } + api_models::enums::WalletIssuer::GooglePay => { + Ok(PaymentInstrument::Googlepay(WalletPayment { + payment_type: PaymentType::Googlepay, + wallet_token: wallet.token, + ..WalletPayment::default() + })) + } + _ => Err(errors::ConnectorError::NotImplemented("Wallet Type".to_string()).into()), + }, + _ => { + Err(errors::ConnectorError::NotImplemented("Current Payment Method".to_string()).into()) + } + } +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for WorldpayPaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + Ok(Self { + instruction: Instruction { + value: PaymentValue { + amount: item.request.amount, + currency: item.request.currency.to_string(), + }, + narrative: InstructionNarrative { + line1: item.merchant_id.clone(), + ..Default::default() + }, + payment_instrument: fetch_payment_instrument( + item.request.payment_method_data.clone(), + )?, + debt_repayment: None, + }, + merchant: Merchant { + entity: item.payment_id.clone(), + ..Default::default() + }, + transaction_reference: item.attempt_id.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "attempt_id".to_string(), + }, + )?, + channel: None, + customer: None, + }) + } +} + +pub struct WorldpayAuthType { + pub(super) api_key: String, +} + +impl TryFrom<&types::ConnectorAuthType> for WorldpayAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.to_string(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType)?, + } + } +} + +impl From for enums::AttemptStatus { + fn from(item: Outcome) -> Self { + match item { + Outcome::Authorized => Self::Authorized, + Outcome::Refused => Self::Failure, + } + } +} + +impl From for enums::AttemptStatus { + fn from(value: EventType) -> Self { + match value { + EventType::Authorized => Self::Authorized, + EventType::CaptureFailed => Self::CaptureFailed, + EventType::Refused => Self::Failure, + EventType::Charged => Self::Charged, + _ => Self::Pending, + } + } +} + +impl From for enums::RefundStatus { + fn from(value: EventType) -> Self { + match value { + EventType::Refunded => Self::Success, + EventType::RefundFailed => Self::Failure, + _ => Self::Pending, + } + } +} + +impl TryFrom> + for types::PaymentsAuthorizeRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::PaymentsResponseRouterData, + ) -> Result { + Ok(Self { + status: match item.response.outcome { + Some(outcome) => enums::AttemptStatus::from(outcome), + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "outcome".to_string(), + })?, + }, + description: item.response.description, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::try_from(item.response.links)?, + redirection_data: None, + redirect: false, + mandate_reference: None, + }), + ..item.data + }) + } +} + +impl TryFrom<&types::RefundsRouterData> for WorldpayRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + Ok(Self { + reference: item.request.connector_transaction_id.clone(), + value: PaymentValue { + amount: item.request.amount, + currency: item.request.currency.to_string(), + }, + }) + } +} diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index e09e2ffd66..fa9688f4de 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -241,8 +241,8 @@ pub enum ApiClientError { #[error("URL encoding of request payload failed")] UrlEncodingFailed, - #[error("Failed to send request to connector")] - RequestNotSent, + #[error("Failed to send request to connector {0}")] + RequestNotSent(String), #[error("Failed to decode response")] ResponseDecodingFailed, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index da71234f4c..0fa67b6eee 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -79,6 +79,7 @@ where merchant_id: merchant_account.merchant_id.clone(), connector: merchant_connector_account.connector_name, payment_id: payment_data.payment_attempt.payment_id.clone(), + attempt_id: Some(payment_data.payment_attempt.attempt_id.clone()), status: payment_data.payment_attempt.status, payment_method, connector_auth_type: auth_type, diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index d2edd3e42a..a839d1d4b6 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -66,6 +66,7 @@ pub async fn construct_refund_router_data<'a, F>( merchant_id: merchant_account.merchant_id.clone(), connector: merchant_connector_account.connector_name, payment_id: payment_attempt.payment_id.clone(), + attempt_id: Some(payment_attempt.attempt_id.clone()), status, payment_method: payment_method_type, connector_auth_type: auth_type, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index b76d61c12c..80d0c5f33f 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -245,7 +245,7 @@ async fn send_request( } .map_err(|error| match error { error if error.is_timeout() => errors::ApiClientError::RequestTimeoutReceived, - _ => errors::ApiClientError::RequestNotSent, + _ => errors::ApiClientError::RequestNotSent(error.to_string()), }) .into_report() .attach_printable("Unable to send request to connector") diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 4e0837ec98..2492fe6b16 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -69,6 +69,7 @@ pub struct RouterData { pub merchant_id: String, pub connector: String, pub payment_id: String, + pub attempt_id: Option, pub status: storage_enums::AttemptStatus, pub payment_method: storage_enums::PaymentMethodType, pub connector_auth_type: ConnectorAuthType, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index fb037b1a21..1ecc0c793b 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -149,6 +149,7 @@ impl ConnectorData { "applepay" => Ok(Box::new(&connector::Applepay)), "cybersource" => Ok(Box::new(&connector::Cybersource)), "shift4" => Ok(Box::new(&connector::Shift4)), + "worldpay" => Ok(Box::new(&connector::Worldpay)), _ => Err(report!(errors::UnexpectedError) .attach_printable(format!("invalid connector name: {connector_name}"))) .change_context(errors::ConnectorError::InvalidConnectorName) diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index f56cdec15b..170e86d664 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -22,6 +22,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { merchant_id: String::from("aci"), connector: "aci".to_string(), payment_id: uuid::Uuid::new_v4().to_string(), + attempt_id: None, status: enums::AttemptStatus::default(), auth_type: enums::AuthenticationType::NoThreeDs, payment_method: enums::PaymentMethodType::Card, @@ -68,6 +69,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { merchant_id: String::from("aci"), connector: "aci".to_string(), payment_id: uuid::Uuid::new_v4().to_string(), + attempt_id: None, status: enums::AttemptStatus::default(), router_return_url: None, payment_method: enums::PaymentMethodType::Card, diff --git a/crates/router/tests/connectors/authorizedotnet.rs b/crates/router/tests/connectors/authorizedotnet.rs index 5225f15547..b8bd87e325 100644 --- a/crates/router/tests/connectors/authorizedotnet.rs +++ b/crates/router/tests/connectors/authorizedotnet.rs @@ -22,6 +22,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { merchant_id: String::from("authorizedotnet"), connector: "authorizedotnet".to_string(), payment_id: uuid::Uuid::new_v4().to_string(), + attempt_id: None, status: enums::AttemptStatus::default(), router_return_url: None, payment_method: enums::PaymentMethodType::Card, @@ -69,6 +70,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { merchant_id: String::from("authorizedotnet"), connector: "authorizedotnet".to_string(), payment_id: uuid::Uuid::new_v4().to_string(), + attempt_id: None, status: enums::AttemptStatus::default(), router_return_url: None, auth_type: enums::AuthenticationType::NoThreeDs, diff --git a/crates/router/tests/connectors/checkout.rs b/crates/router/tests/connectors/checkout.rs index 0038b357e6..7e3adddf15 100644 --- a/crates/router/tests/connectors/checkout.rs +++ b/crates/router/tests/connectors/checkout.rs @@ -19,6 +19,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { merchant_id: "checkout".to_string(), connector: "checkout".to_string(), payment_id: uuid::Uuid::new_v4().to_string(), + attempt_id: None, status: enums::AttemptStatus::default(), router_return_url: None, auth_type: enums::AuthenticationType::NoThreeDs, @@ -66,6 +67,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { merchant_id: "checkout".to_string(), connector: "checkout".to_string(), payment_id: uuid::Uuid::new_v4().to_string(), + attempt_id: None, status: enums::AttemptStatus::default(), router_return_url: None, payment_method: enums::PaymentMethodType::Card, diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index 5be73e5aaa..decacf4d6b 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -7,6 +7,7 @@ pub(crate) struct ConnectorAuthentication { pub authorizedotnet: Option, pub checkout: Option, pub shift4: Option, + pub worldpay: Option, } impl ConnectorAuthentication { diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index b4ea15522d..3f74d71ed3 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -6,3 +6,4 @@ mod checkout; mod connector_auth; mod shift4; mod utils; +mod worldpay; diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 23b7a383ab..eedea2a0ad 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -14,4 +14,7 @@ api_key = "Bearer MyApiKey" key1 = "MyProcessingChannelId" [shift4] +api_key = "Bearer MyApiKey" + +[worldpay] api_key = "Bearer MyApiKey" \ No newline at end of file diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index b5f55f4e87..5ae3ccb886 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -8,6 +8,7 @@ use router::{ routes, services, types::{self, api, storage::enums, PaymentAddress}, }; +use wiremock::{Mock, MockServer}; pub trait Connector { fn get_data(&self) -> types::api::ConnectorData; @@ -25,6 +26,7 @@ pub trait ConnectorActions: Connector { let request = generate_data( self.get_name(), self.get_auth_token(), + enums::AuthenticationType::NoThreeDs, payment_data.unwrap_or_else(|| types::PaymentsAuthorizeData { capture_method: Some(storage_models::enums::CaptureMethod::Manual), ..PaymentAuthorizeType::default().0 @@ -40,10 +42,26 @@ pub trait ConnectorActions: Connector { let request = generate_data( self.get_name(), self.get_auth_token(), + enums::AuthenticationType::NoThreeDs, payment_data.unwrap_or_else(|| PaymentAuthorizeType::default().0), ); call_connector(request, integration).await } + + async fn sync_payment( + &self, + payment_data: Option, + ) -> types::PaymentsSyncRouterData { + let integration = self.get_data().connector.get_connector_integration(); + let request = generate_data( + self.get_name(), + self.get_auth_token(), + enums::AuthenticationType::NoThreeDs, + payment_data.unwrap_or_else(|| PaymentSyncType::default().0), + ); + call_connector(request, integration).await + } + async fn capture_payment( &self, transaction_id: String, @@ -53,6 +71,7 @@ pub trait ConnectorActions: Connector { let request = generate_data( self.get_name(), self.get_auth_token(), + enums::AuthenticationType::NoThreeDs, payment_data.unwrap_or(types::PaymentsCaptureData { amount_to_capture: Some(100), connector_transaction_id: transaction_id, @@ -62,6 +81,25 @@ pub trait ConnectorActions: Connector { ); call_connector(request, integration).await } + + async fn void_payment( + &self, + transaction_id: String, + payment_data: Option, + ) -> types::PaymentsCancelRouterData { + let integration = self.get_data().connector.get_connector_integration(); + let request = generate_data( + self.get_name(), + self.get_auth_token(), + enums::AuthenticationType::NoThreeDs, + payment_data.unwrap_or(types::PaymentsCancelData { + connector_transaction_id: transaction_id, + cancellation_reason: Some("Test cancellation".to_string()), + }), + ); + call_connector(request, integration).await + } + async fn refund_payment( &self, transaction_id: String, @@ -71,6 +109,29 @@ pub trait ConnectorActions: Connector { let request = generate_data( self.get_name(), self.get_auth_token(), + enums::AuthenticationType::NoThreeDs, + payment_data.unwrap_or_else(|| types::RefundsData { + amount: 100, + currency: enums::Currency::USD, + refund_id: uuid::Uuid::new_v4().to_string(), + payment_method_data: types::api::PaymentMethod::Card(CCardType::default().0), + connector_transaction_id: transaction_id, + refund_amount: 100, + }), + ); + call_connector(request, integration).await + } + + async fn sync_refund( + &self, + transaction_id: String, + payment_data: Option, + ) -> types::RefundSyncRouterData { + let integration = self.get_data().connector.get_connector_integration(); + let request = generate_data( + self.get_name(), + self.get_auth_token(), + enums::AuthenticationType::NoThreeDs, payment_data.unwrap_or_else(|| types::RefundsData { amount: 100, currency: enums::Currency::USD, @@ -105,7 +166,32 @@ async fn call_connector< .unwrap() } +pub struct MockConfig { + pub address: Option, + pub mocks: Vec, +} + +#[async_trait] +pub trait LocalMock { + async fn start_server(&self, config: MockConfig) -> MockServer { + let address = config + .address + .unwrap_or_else(|| "127.0.0.1:9090".to_string()); + let listener = std::net::TcpListener::bind(address).unwrap(); + let expected_server_address = listener + .local_addr() + .expect("Failed to get server address."); + let mock_server = MockServer::builder().listener(listener).start().await; + assert_eq!(&expected_server_address, mock_server.address()); + for mock in config.mocks { + mock_server.register(mock).await; + } + mock_server + } +} + pub struct PaymentAuthorizeType(pub types::PaymentsAuthorizeData); +pub struct PaymentSyncType(pub types::PaymentsSyncData); pub struct PaymentRefundType(pub types::RefundsData); pub struct CCardType(pub api::CCard); @@ -142,6 +228,18 @@ impl Default for PaymentAuthorizeType { } } +impl Default for PaymentSyncType { + fn default() -> Self { + let data = types::PaymentsSyncData { + connector_transaction_id: types::ResponseId::ConnectorTransactionId( + "12345".to_string(), + ), + encoded_data: None, + }; + Self(data) + } +} + impl Default for PaymentRefundType { fn default() -> Self { let data = types::RefundsData { @@ -171,6 +269,7 @@ pub fn get_connector_transaction_id( fn generate_data, Res>( connector: String, connector_auth_type: types::ConnectorAuthType, + auth_type: enums::AuthenticationType, req: Req, ) -> types::RouterData { types::RouterData { @@ -178,9 +277,10 @@ fn generate_data, Res>( merchant_id: connector.clone(), connector, payment_id: uuid::Uuid::new_v4().to_string(), + attempt_id: Some(uuid::Uuid::new_v4().to_string()), status: enums::AttemptStatus::default(), router_return_url: None, - auth_type: enums::AuthenticationType::NoThreeDs, + auth_type, payment_method: enums::PaymentMethodType::Card, connector_auth_type, description: Some("This is a test".to_string()), diff --git a/crates/router/tests/connectors/worldpay.rs b/crates/router/tests/connectors/worldpay.rs new file mode 100644 index 0000000000..3a123239a1 --- /dev/null +++ b/crates/router/tests/connectors/worldpay.rs @@ -0,0 +1,320 @@ +use futures::future::OptionFuture; +use router::types::{ + self, + api::{self, enums as api_enums}, + storage::enums, +}; +use serde_json::json; +use serial_test::serial; +use wiremock::{ + matchers::{body_json, method, path}, + Mock, ResponseTemplate, +}; + +use crate::{ + connector_auth, + utils::{self, ConnectorActions, LocalMock, MockConfig}, +}; + +struct Worldpay; + +impl LocalMock for Worldpay {} +impl ConnectorActions for Worldpay {} +impl utils::Connector for Worldpay { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Worldpay; + types::api::ConnectorData { + connector: Box::new(&Worldpay), + connector_name: types::Connector::Worldpay, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .worldpay + .expect("Missing connector authentication configuration"), + ) + } + + fn get_name(&self) -> String { + "worldpay".to_string() + } +} + +#[actix_web::test] +#[serial] +async fn should_authorize_card_payment() { + let conn = Worldpay {}; + let _mock = conn.start_server(get_mock_config()).await; + let response = conn.authorize_payment(None).await; + assert_eq!(response.status, enums::AttemptStatus::Authorized); + assert_eq!( + utils::get_connector_transaction_id(response), + Some("123456".to_string()) + ); +} + +#[actix_web::test] +#[serial] +async fn should_authorize_gpay_payment() { + let conn = Worldpay {}; + let _mock = conn.start_server(get_mock_config()).await; + let response = conn + .authorize_payment(Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Wallet(api::WalletData { + issuer_name: api_enums::WalletIssuer::GooglePay, + token: "someToken".to_string(), + }), + ..utils::PaymentAuthorizeType::default().0 + })) + .await; + assert_eq!(response.status, enums::AttemptStatus::Authorized); + assert_eq!( + utils::get_connector_transaction_id(response), + Some("123456".to_string()) + ); +} + +#[actix_web::test] +#[serial] +async fn should_authorize_applepay_payment() { + let conn = Worldpay {}; + let _mock = conn.start_server(get_mock_config()).await; + let response = conn + .authorize_payment(Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Wallet(api::WalletData { + issuer_name: api_enums::WalletIssuer::ApplePay, + token: "someToken".to_string(), + }), + ..utils::PaymentAuthorizeType::default().0 + })) + .await; + assert_eq!(response.status, enums::AttemptStatus::Authorized); + assert_eq!( + utils::get_connector_transaction_id(response), + Some("123456".to_string()) + ); +} + +#[actix_web::test] +#[serial] +async fn should_capture_already_authorized_payment() { + let connector = Worldpay {}; + let _mock = connector.start_server(get_mock_config()).await; + let authorize_response = connector.authorize_payment(None).await; + assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); + let txn_id = utils::get_connector_transaction_id(authorize_response); + let response: OptionFuture<_> = txn_id + .map(|transaction_id| async move { + connector.capture_payment(transaction_id, None).await.status + }) + .into(); + assert_eq!(response.await, Some(enums::AttemptStatus::Charged)); +} + +#[actix_web::test] +#[serial] +async fn should_sync_payment() { + let connector = Worldpay {}; + let _mock = connector.start_server(get_mock_config()).await; + let response = connector + .sync_payment(Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + "112233".to_string(), + ), + encoded_data: None, + })) + .await; + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +#[actix_web::test] +#[serial] +async fn should_void_already_authorized_payment() { + let connector = Worldpay {}; + let _mock = connector.start_server(get_mock_config()).await; + let authorize_response = connector.authorize_payment(None).await; + assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); + let txn_id = utils::get_connector_transaction_id(authorize_response); + let response: OptionFuture<_> = + txn_id + .map(|transaction_id| async move { + connector.void_payment(transaction_id, None).await.status + }) + .into(); + assert_eq!(response.await, Some(enums::AttemptStatus::Voided)); +} + +#[actix_web::test] +#[serial] +async fn should_fail_capture_for_invalid_payment() { + let connector = Worldpay {}; + let _mock = connector.start_server(get_mock_config()).await; + let authorize_response = connector.authorize_payment(None).await; + assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); + let response = connector.capture_payment("12345".to_string(), None).await; + let err = response.response.unwrap_err(); + assert_eq!( + err.message, + "You must provide valid transaction id to capture payment".to_string() + ); + assert_eq!(err.code, "invalid-id".to_string()); +} + +#[actix_web::test] +#[serial] +async fn should_refund_succeeded_payment() { + let connector = Worldpay {}; + let _mock = connector.start_server(get_mock_config()).await; + //make a successful payment + let response = connector.make_payment(None).await; + + //try refund for previous payment + let transaction_id = utils::get_connector_transaction_id(response).unwrap(); + let response = connector.refund_payment(transaction_id, None).await; + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +#[actix_web::test] +#[serial] +async fn should_sync_refund() { + let connector = Worldpay {}; + let _mock = connector.start_server(get_mock_config()).await; + let response = connector.sync_refund("654321".to_string(), None).await; + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +fn get_mock_config() -> MockConfig { + let authorized = json!({ + "outcome": "authorized", + "_links": { + "payments:cancel": { + "href": "/payments/authorizations/cancellations/123456" + }, + "payments:settle": { + "href": "/payments/settlements/123456" + }, + "payments:partialSettle": { + "href": "/payments/settlements/partials/123456" + }, + "payments:events": { + "href": "/payments/events/123456" + }, + "curies": [ + { + "name": "payments", + "href": "/rels/payments/{rel}", + "templated": true + } + ] + } + }); + let settled = json!({ + "_links": { + "payments:refund": { + "href": "/payments/settlements/refunds/full/654321" + }, + "payments:partialRefund": { + "href": "/payments/settlements/refunds/partials/654321" + }, + "payments:events": { + "href": "/payments/events/654321" + }, + "curies": [ + { + "name": "payments", + "href": "/rels/payments/{rel}", + "templated": true + } + ] + } + }); + let error_resp = json!({ + "errorName": "invalid-id", + "message": "You must provide valid transaction id to capture payment" + }); + let partial_refund = json!({ + "_links": { + "payments:events": { + "href": "https://try.access.worldpay.com/payments/events/eyJrIjoiazNhYjYzMiJ9" + }, + "curies": [{ + "name": "payments", + "href": "https://try.access.worldpay.com/rels/payments/{rel}", + "templated": true + }] + } + }); + let partial_refund_req_body = json!({ + "value": { + "amount": 100, + "currency": "USD" + }, + "reference": "123456" + }); + let refunded = json!({ + "lastEvent": "refunded", + "_links": { + "payments:cancel": "/payments/authorizations/cancellations/654321", + "payments:settle": "/payments/settlements/full/654321", + "payments:partialSettle": "/payments/settlements/partials/654321", + "curies": [ + { + "name": "payments", + "href": "/rels/payments/{rel}", + "templated": true + } + ] + } + }); + let sync_payment = json!({ + "lastEvent": "authorized", + "_links": { + "payments:events": "/payments/authorizations/events/654321", + "payments:settle": "/payments/settlements/full/654321", + "payments:partialSettle": "/payments/settlements/partials/654321", + "curies": [ + { + "name": "payments", + "href": "/rels/payments/{rel}", + "templated": true + } + ] + } + }); + + MockConfig { + address: Some("127.0.0.1:9090".to_string()), + mocks: vec![ + Mock::given(method("POST")) + .and(path("/payments/authorizations".to_string())) + .respond_with(ResponseTemplate::new(201).set_body_json(authorized)), + Mock::given(method("POST")) + .and(path("/payments/settlements/123456".to_string())) + .respond_with(ResponseTemplate::new(202).set_body_json(settled)), + Mock::given(method("GET")) + .and(path("/payments/events/112233".to_string())) + .respond_with(ResponseTemplate::new(200).set_body_json(sync_payment)), + Mock::given(method("POST")) + .and(path("/payments/settlements/12345".to_string())) + .respond_with(ResponseTemplate::new(400).set_body_json(error_resp)), + Mock::given(method("POST")) + .and(path( + "/payments/settlements/refunds/partials/123456".to_string(), + )) + .and(body_json(partial_refund_req_body)) + .respond_with(ResponseTemplate::new(202).set_body_json(partial_refund)), + Mock::given(method("GET")) + .and(path("/payments/events/654321".to_string())) + .respond_with(ResponseTemplate::new(200).set_body_json(refunded)), + ], + } +} diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 31d02d185d..1bd23546b3 100644 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -2,23 +2,43 @@ pg=$1; pgc="$(tr '[:lower:]' '[:upper:]' <<< ${pg:0:1})${pg:1}" src="crates/router/src" conn="$src/connector" +tests="../../tests/connectors/" SCRIPT="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" if [[ -z "$pg" ]]; then echo 'Connector name not present: try "sh add_connector.sh "' exit fi cd $SCRIPT/.. +# remove template files if already created for this connector rm -rf $conn/$pg $conn/$pg.rs -git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs +git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/Development.toml config/docker_compose.toml crates/api_models/src/enums.rs +# add enum for this connector in required places sed -i'' -e "s/pub use self::{/pub mod ${pg};\n\npub use self::{/" $conn.rs sed -i'' -e "s/};/${pg}::${pgc},\n};/" $conn.rs sed -i'' -e "s/_ => Err/\"${pg}\" => Ok(Box::new(\&connector::${pgc})),\n\t\t\t_ => Err/" $src/types/api.rs sed -i'' -e "s/pub supported: SupportedConnectors,/pub supported: SupportedConnectors,\n\tpub ${pg}: ConnectorParams,/" $src/configs/settings.rs -rm $conn.rs-e $src/types/api.rs-e $src/configs/settings.rs-e +sed -i'' -e "s/\[scheduler\]/[connectors.${pg}]\nbase_url = \"\"\n\n[scheduler]/" config/Development.toml +sed -r -i'' -e "s/cards = \[(.*)\]/cards = [\1, \"${pg}\"]/" config/Development.toml +sed -i'' -e "s/\[connectors.supported\]/[connectors.${pg}]\nbase_url = ""\n\n[connectors.supported]/" config/docker_compose.toml +sed -r -i'' -e "s/cards = \[(.*)\]/cards = [\1, \"${pg}\"]/" config/docker_compose.toml +sed -i'' -e "s/Dummy,/Dummy,\n\t${pgc},/" crates/api_models/src/enums.rs +# remove temporary files created in above step +rm $conn.rs-e $src/types/api.rs-e $src/configs/settings.rs-e config/Development.toml-e config/docker_compose.toml-e crates/api_models/src/enums.rs-e cd $conn/ +# generate template files for the connector +cargo install cargo-generate cargo gen-pg $pg +# move sub files and test files to appropiate folder mv $pg/mod.rs $pg.rs -mv $pg/test.rs ../../tests/connectors/$pg.rs -sed -i'' -e "s/mod utils;/mod ${pg};\nmod utils;/" ../../tests/connectors/main.rs -rm ../../tests/connectors/main.rs-e -echo "Successfully created connector: try running the tests of "$pg.rs \ No newline at end of file +mv $pg/test.rs ${tests}/$pg.rs +# remove changes from tests if already done for this connector +git checkout ${tests}/main.rs ${tests}/connector_auth.rs +# add enum for this connector in test folder +sed -i'' -e "s/mod utils;/mod ${pg};\nmod utils;/" ${tests}/main.rs +sed -i'' -e "s/struct ConnectorAuthentication {/struct ConnectorAuthentication {\n\tpub ${pg}: Option,/" ${tests}/connector_auth.rs +# remove temporary files created in above step +rm ${tests}/main.rs-e ${tests}/connector_auth.rs-e +cargo build +echo "Successfully created connector. Running the tests of "$pg.rs +# runs tests for the new connector +cargo test --package router --test connectors -- $pg \ No newline at end of file