feature(connector): add support for worldpay connector (#272)

This commit is contained in:
Jagan
2023-01-09 12:56:03 +05:30
committed by GitHub
parent 6a0d183e7b
commit 68f92797db
31 changed files with 2198 additions and 104 deletions

279
Cargo.lock generated
View File

@ -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"

View File

@ -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 <connector-name>
cd scripts
sh add_connector.sh <connector-name>
```
For this tutorial `<connector-name>` 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 @@ Dont 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/<connector-name>` 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="<CONNECTOR-NAME>" #Change it to appropriate connector name
export SCHEMA_PATH="<PATH-TO-JSON-SCHEMA-FILE>" #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
```

View File

@ -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"

View File

@ -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"]

View File

@ -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"]

View File

@ -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<Flow, Request, Response>(
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for {{project-name | downcase | pascal_case}}
where
Self: ConnectorIntegration<Flow, Request, Response>,{
fn build_headers(
&self,
req: &types::RouterData<Flow, Request, Response>,
_req: &types::RouterData<Flow, Request, Response>,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, 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<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for {{project-name | downcase | pascal_case}}
{
fn get_headers(
&self,
req: &types::PaymentsSyncRouterData,
_req: &types::PaymentsSyncRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, 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<String, errors::ConnectorError> {
todo!()
}
fn build_request(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
_req: &types::PaymentsSyncRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
todo!()
}
fn get_error_response(
&self,
res: Bytes,
_res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
todo!()
}
fn handle_response(
&self,
data: &types::PaymentsSyncRouterData,
res: Response,
_data: &types::PaymentsSyncRouterData,
_res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
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<Vec<(String, String)>, 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<Option<services::Request>, errors::ConnectorError> {
todo!()
}
fn handle_response(
&self,
data: &types::PaymentsCaptureRouterData,
res: Response,
_data: &types::PaymentsCaptureRouterData,
_res: Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
todo!()
}
fn get_url(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
_req: &types::PaymentsCaptureRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
todo!()
}
fn get_error_response(
&self,
res: Bytes,
_res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
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<Vec<(String, String)>,errors::ConnectorError> {
fn get_headers(&self, _req: &types::PaymentsAuthorizeRouterData,_connectors: &settings::Connectors,) -> CustomResult<Vec<(String, String)>,errors::ConnectorError> {
todo!()
}
@ -211,13 +216,13 @@ impl
todo!()
}
fn get_url(&self, _req: &types::PaymentsAuthorizeRouterData, connectors: &settings::Connectors,) -> CustomResult<String,errors::ConnectorError> {
fn get_url(&self, _req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors,) -> CustomResult<String,errors::ConnectorError> {
todo!()
}
fn get_request_body(&self, req: &types::PaymentsAuthorizeRouterData) -> CustomResult<Option<String>,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<api::Execute>) -> CustomResult<Vec<(String,String)>,errors::ConnectorError> {
fn get_headers(&self, _req: &types::RefundsRouterData<api::Execute>,_connectors: &settings::Connectors,) -> CustomResult<Vec<(String,String)>,errors::ConnectorError> {
todo!()
}
@ -260,12 +265,12 @@ impl
todo!()
}
fn get_url(&self, _req: &types::RefundsRouterData<api::Execute>, connectors: &settings::Connectors,) -> CustomResult<String,errors::ConnectorError> {
fn get_url(&self, _req: &types::RefundsRouterData<api::Execute>, _connectors: &settings::Connectors,) -> CustomResult<String,errors::ConnectorError> {
todo!()
}
fn get_request_body(&self, req: &types::RefundsRouterData<api::Execute>) -> CustomResult<Option<String>,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<api::RSync, types::RefundsData, types::RefundsResponseData> for {{project-name | downcase | pascal_case}} {
fn get_headers(&self, _req: &types::RefundSyncRouterData) -> CustomResult<Vec<(String, String)>,errors::ConnectorError> {
ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData> for {{project-name | downcase | pascal_case}} {
fn get_headers(&self, _req: &types::RefundSyncRouterData,_connectors: &settings::Connectors,) -> CustomResult<Vec<(String, String)>,errors::ConnectorError> {
todo!()
}
@ -341,21 +346,21 @@ impl api::IncomingWebhook for {{project-name | downcase | pascal_case}} {
&self,
_body: &[u8],
) -> CustomResult<String, errors::ConnectorError> {
todo!()
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
fn get_webhook_event_type(
&self,
_body: &[u8],
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
todo!()
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
fn get_webhook_resource_object(
&self,
_body: &[u8],
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
todo!()
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
}

View File

@ -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}};

View File

@ -497,6 +497,7 @@ pub enum Connector {
Klarna,
Shift4,
Stripe,
Worldpay,
}
impl From<AttemptStatus> for IntentStatus {

View File

@ -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"

View File

@ -117,6 +117,7 @@ pub struct Connectors {
pub shift4: ConnectorParams,
pub stripe: ConnectorParams,
pub supported: SupportedConnectors,
pub worldpay: ConnectorParams,
pub applepay: ConnectorParams,
}

View File

@ -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,
};

View File

@ -439,6 +439,21 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
Ok(format!("{}refunds", self.base_url(connectors),))
}
fn build_request(
&self,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, 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,

View File

@ -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<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Worldpay
where
Self: ConnectorIntegration<Flow, Request, Response>,
{
fn build_headers(
&self,
req: &types::RouterData<Flow, Request, Response>,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, 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<Vec<(String, String)>, 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<ErrorResponse, errors::ConnectorError> {
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<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
for Worldpay
{
}
impl api::PaymentVoid for Worldpay {}
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Worldpay
{
fn get_headers(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, 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<String, errors::ConnectorError> {
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<Option<services::Request>, 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<types::PaymentsCancelRouterData, errors::ConnectorError>
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<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::PaymentSync for Worldpay {}
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Worldpay
{
fn get_headers(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, 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<String, errors::ConnectorError> {
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<Option<services::Request>, 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<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
fn handle_response(
&self,
data: &types::PaymentsSyncRouterData,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
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<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
for Worldpay
{
fn get_headers(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, 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<Option<services::Request>, 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<types::PaymentsCaptureRouterData, errors::ConnectorError> {
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<String, errors::ConnectorError> {
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<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::PaymentSession for Worldpay {}
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Worldpay
{
}
impl api::PaymentAuthorize for Worldpay {}
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
for Worldpay
{
fn get_headers(
&self,
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, 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<String, errors::ConnectorError> {
Ok(format!(
"{}payments/authorizations",
self.base_url(connectors)
))
}
fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let worldpay_req = utils::Encode::<WorldpayPaymentsRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(worldpay_req))
}
fn build_request(
&self,
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, 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<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
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<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::Refund for Worldpay {}
impl api::RefundExecute for Worldpay {}
impl api::RefundSync for Worldpay {}
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
for Worldpay
{
fn get_headers(
&self,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, 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<Option<String>, errors::ConnectorError> {
let req = utils::Encode::<WorldpayRefundRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(req))
}
fn get_url(
&self,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
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<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, 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<api::Execute>,
res: Response,
) -> CustomResult<types::RefundsRouterData<api::Execute>, 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<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData> for Worldpay {
fn get_headers(
&self,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, 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<String, errors::ConnectorError> {
Ok(format!(
"{}payments/events/{}",
self.base_url(connectors),
req.request.connector_transaction_id
))
}
fn build_request(
&self,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, 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<types::RefundSyncRouterData, errors::ConnectorError> {
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<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
#[async_trait::async_trait]
impl api::IncomingWebhook for Worldpay {
fn get_webhook_object_reference_id(
&self,
_body: &[u8],
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
fn get_webhook_event_type(
&self,
_body: &[u8],
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
fn get_webhook_resource_object(
&self,
_body: &[u8],
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
}
impl services::ConnectorRedirectResponse for Worldpay {
fn get_flow_type(
&self,
_query_params: &str,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
Ok(payments::CallConnectorAction::Trigger)
}
}

View File

@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address2: Option<String>,
pub postal_code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address3: Option<String>,
pub country_code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub address1: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorldpayPaymentsRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<Channel>,
pub instruction: Instruction,
#[serde(skip_serializing_if = "Option::is_none")]
pub customer: Option<Customer>,
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authentication: Option<CustomerAuthentication>,
}
#[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<String>,
pub version: ThreeDSVersion,
#[serde(skip_serializing_if = "Option::is_none")]
pub transaction_id: Option<String>,
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<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Instruction {
#[serde(skip_serializing_if = "Option::is_none")]
pub debt_repayment: Option<bool>,
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<String>,
}
#[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<BillingAddress>,
#[serde(skip_serializing_if = "Option::is_none")]
pub card_holder_name: Option<String>,
pub card_expiry_date: CardExpiryDate,
#[serde(skip_serializing_if = "Option::is_none")]
pub cvc: Option<String>,
#[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<BillingAddress>,
}
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_facilitator: Option<PaymentFacilitator>,
}
#[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<String>,
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<String>,
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<String>,
}
#[derive(Default, Debug, Serialize)]
pub struct WorldpayRefundRequest {
pub value: PaymentValue,
pub reference: String,
}

View File

@ -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<Exemption>,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer: Option<Issuer>,
pub outcome: Option<Outcome>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_instrument: Option<PaymentsResPaymentInstrument>,
/// 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<Vec<RiskFactorsInner>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scheme: Option<PaymentsResponseScheme>,
#[serde(rename = "_links", skip_serializing_if = "Option::is_none")]
pub links: Option<PaymentLinks>,
}
#[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<EventLinks>,
}
#[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<PaymentLink>,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct EventLinks {
#[serde(rename = "payments:events", skip_serializing_if = "Option::is_none")]
pub events: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct PaymentLink {
pub href: String,
}
fn get_resource_id<T, F>(
links: Option<PaymentLinks>,
transform_fn: F,
) -> Result<T, error_stack::Report<errors::ConnectorError>>
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<Option<PaymentLinks>> for ResponseIdStr {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(links: Option<PaymentLinks>) -> Result<Self, Self::Error> {
get_resource_id(links, |id| Self { id })
}
}
impl TryFrom<Option<PaymentLinks>> for types::ResponseId {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(links: Option<PaymentLinks>) -> Result<Self, Self::Error> {
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub card: Option<PaymentInstrumentCard>,
}
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<PaymentInstrumentCardNumber>,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer: Option<PaymentInstrumentCardIssuer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_account_reference: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub country_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub funding_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub brand: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expiry_date: Option<PaymentInstrumentCardExpiryDate>,
}
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<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub year: Option<i32>,
}
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<String>,
}
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last4_digits: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dpan: Option<String>,
}
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<Detail>,
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,
}

View File

@ -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<T: FromStr>(
val: masking::Secret<String, masking::WithType>,
) -> CustomResult<T, errors::ConnectorError>
where
<T as FromStr>::Err: Sync,
{
let res = val.peek().parse::<T>();
if let Ok(val) = res {
Ok(val)
} else {
Err(errors::ConnectorError::RequestEncodingFailed)?
}
}
fn fetch_payment_instrument(
payment_method: api::PaymentMethod,
) -> CustomResult<PaymentInstrument, errors::ConnectorError> {
match payment_method {
api::PaymentMethod::Card(card) => Ok(PaymentInstrument::Card(CardPayment {
card_expiry_date: CardExpiryDate {
month: parse_int::<u8>(card.card_exp_month)?,
year: parse_int::<u16>(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<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
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<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
match auth_type {
types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self {
api_key: api_key.to_string(),
}),
_ => Err(errors::ConnectorError::FailedToObtainAuthType)?,
}
}
}
impl From<Outcome> for enums::AttemptStatus {
fn from(item: Outcome) -> Self {
match item {
Outcome::Authorized => Self::Authorized,
Outcome::Refused => Self::Failure,
}
}
}
impl From<EventType> 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<EventType> for enums::RefundStatus {
fn from(value: EventType) -> Self {
match value {
EventType::Refunded => Self::Success,
EventType::RefundFailed => Self::Failure,
_ => Self::Pending,
}
}
}
impl TryFrom<types::PaymentsResponseRouterData<WorldpayPaymentsResponse>>
for types::PaymentsAuthorizeRouterData
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::PaymentsResponseRouterData<WorldpayPaymentsResponse>,
) -> Result<Self, Self::Error> {
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<F> TryFrom<&types::RefundsRouterData<F>> for WorldpayRefundRequest {
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
Ok(Self {
reference: item.request.connector_transaction_id.clone(),
value: PaymentValue {
amount: item.request.amount,
currency: item.request.currency.to_string(),
},
})
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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")

View File

@ -69,6 +69,7 @@ pub struct RouterData<Flow, Request, Response> {
pub merchant_id: String,
pub connector: String,
pub payment_id: String,
pub attempt_id: Option<String>,
pub status: storage_enums::AttemptStatus,
pub payment_method: storage_enums::PaymentMethodType,
pub connector_auth_type: ConnectorAuthType,

View File

@ -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)

View File

@ -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<F>() -> types::RefundsRouterData<F> {
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,

View File

@ -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<F>() -> types::RefundsRouterData<F> {
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,

View File

@ -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<F>() -> types::RefundsRouterData<F> {
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,

View File

@ -7,6 +7,7 @@ pub(crate) struct ConnectorAuthentication {
pub authorizedotnet: Option<BodyKey>,
pub checkout: Option<BodyKey>,
pub shift4: Option<HeaderKey>,
pub worldpay: Option<HeaderKey>,
}
impl ConnectorAuthentication {

View File

@ -6,3 +6,4 @@ mod checkout;
mod connector_auth;
mod shift4;
mod utils;
mod worldpay;

View File

@ -15,3 +15,6 @@ key1 = "MyProcessingChannelId"
[shift4]
api_key = "Bearer MyApiKey"
[worldpay]
api_key = "Bearer MyApiKey"

View File

@ -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::PaymentsSyncData>,
) -> 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::PaymentsCancelData>,
) -> 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::RefundsData>,
) -> 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<String>,
pub mocks: Vec<Mock>,
}
#[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<Flow, Req: From<Req>, Res>(
connector: String,
connector_auth_type: types::ConnectorAuthType,
auth_type: enums::AuthenticationType,
req: Req,
) -> types::RouterData<Flow, Req, Res> {
types::RouterData {
@ -178,9 +277,10 @@ fn generate_data<Flow, Req: From<Req>, 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()),

View File

@ -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)),
],
}
}

View File

@ -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 <adyen>"'
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
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<HeaderKey>,/" ${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